Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0060f3c19 | ||
|
|
8cb5a7a258 | ||
|
|
484dad1072 |
@@ -117,12 +117,6 @@ opencode.json
|
|||||||
|
|
||||||
# iOS framework build artifacts produced by chanora_bridge.podspec
|
# iOS framework build artifacts produced by chanora_bridge.podspec
|
||||||
/apps/chanora_flutter/ios/Frameworks/
|
/apps/chanora_flutter/ios/Frameworks/
|
||||||
|
|
||||||
# macOS framework build artifacts produced by chanora_bridge.podspec
|
|
||||||
# (prepare_command + script_phase rm -rf and regenerate this tree on
|
|
||||||
# every pod install AND every Xcode build, so tracking it in git is
|
|
||||||
# pure waste — the committed binary was ~40 MB per commit).
|
|
||||||
/apps/chanora_flutter/macos/Frameworks/
|
|
||||||
.opencode/
|
.opencode/
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
Screenshot 2026-05-17 at 22.23.07.png
|
Screenshot 2026-05-17 at 22.23.07.png
|
||||||
|
|||||||
Generated
+1
-1
@@ -419,12 +419,12 @@ name = "chanora_audio"
|
|||||||
version = "0.2.0-beta.1"
|
version = "0.2.0-beta.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"audiopus",
|
"audiopus",
|
||||||
"bytemuck",
|
|
||||||
"chanora_protocol",
|
"chanora_protocol",
|
||||||
"coreaudio-rs",
|
"coreaudio-rs",
|
||||||
"cpal",
|
"cpal",
|
||||||
"criterion",
|
"criterion",
|
||||||
"crossbeam",
|
"crossbeam",
|
||||||
|
"crossbeam-utils",
|
||||||
"dhat",
|
"dhat",
|
||||||
"dispatch2",
|
"dispatch2",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|||||||
@@ -63,10 +63,8 @@ android {
|
|||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlinOptions {
|
||||||
compilerOptions {
|
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
|
|||||||
@@ -1,6 +1,2 @@
|
|||||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
# This builtInKotlin flag was added automatically by Flutter migrator
|
|
||||||
android.builtInKotlin=false
|
|
||||||
# This newDsl flag was added automatically by Flutter migrator
|
|
||||||
android.newDsl=false
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.13.1" apply false
|
id("com.android.application") version "8.11.1" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.3.0" apply false
|
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|||||||
@@ -1,32 +1,69 @@
|
|||||||
PODS:
|
PODS:
|
||||||
|
- audio_session (0.0.1):
|
||||||
|
- Flutter
|
||||||
- chanora_bridge (1.0.0)
|
- chanora_bridge (1.0.0)
|
||||||
|
- connectivity_plus (0.0.1):
|
||||||
|
- Flutter
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_foreground_task (0.0.1):
|
- flutter_foreground_task (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- haptic_kit (1.0.0):
|
- haptic_kit (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- package_info_plus (0.4.5):
|
||||||
|
- Flutter
|
||||||
|
- share_plus (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- shared_preferences_foundation (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
- url_launcher_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
|
- audio_session (from `.symlinks/plugins/audio_session/ios`)
|
||||||
- chanora_bridge (from `.`)
|
- chanora_bridge (from `.`)
|
||||||
|
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`)
|
- flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`)
|
||||||
- haptic_kit (from `.symlinks/plugins/haptic_kit/ios`)
|
- haptic_kit (from `.symlinks/plugins/haptic_kit/ios`)
|
||||||
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
|
audio_session:
|
||||||
|
:path: ".symlinks/plugins/audio_session/ios"
|
||||||
chanora_bridge:
|
chanora_bridge:
|
||||||
:path: "."
|
:path: "."
|
||||||
|
connectivity_plus:
|
||||||
|
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_foreground_task:
|
flutter_foreground_task:
|
||||||
:path: ".symlinks/plugins/flutter_foreground_task/ios"
|
:path: ".symlinks/plugins/flutter_foreground_task/ios"
|
||||||
haptic_kit:
|
haptic_kit:
|
||||||
:path: ".symlinks/plugins/haptic_kit/ios"
|
:path: ".symlinks/plugins/haptic_kit/ios"
|
||||||
|
package_info_plus:
|
||||||
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
|
share_plus:
|
||||||
|
:path: ".symlinks/plugins/share_plus/ios"
|
||||||
|
shared_preferences_foundation:
|
||||||
|
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||||
|
url_launcher_ios:
|
||||||
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
|
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
|
||||||
chanora_bridge: 2ed7c2ba427fab135dd9eab66c507b09cfee113a
|
chanora_bridge: 2ed7c2ba427fab135dd9eab66c507b09cfee113a
|
||||||
|
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89
|
flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89
|
||||||
haptic_kit: b22c4fbb2aa7b0d66f2891f81a9e950ad2de5758
|
haptic_kit: b22c4fbb2aa7b0d66f2891f81a9e950ad2de5758
|
||||||
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
|
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||||
|
|
||||||
PODFILE CHECKSUM: e2123068539aeb66d53dc1612b383d13f489ede2
|
PODFILE CHECKSUM: e2123068539aeb66d53dc1612b383d13f489ede2
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
FD3C80659716BF7A0C95C7AF /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1937FD83C5CC909094CDC137 /* PrivacyInfo.xcprivacy */; };
|
FD3C80659716BF7A0C95C7AF /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1937FD83C5CC909094CDC137 /* PrivacyInfo.xcprivacy */; };
|
||||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -73,7 +72,6 @@
|
|||||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
C10A61C706CAF223682AC397 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
C10A61C706CAF223682AC397 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
E469085D9AE850FF6BD35704 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
E469085D9AE850FF6BD35704 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -81,7 +79,6 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
|
||||||
8C5000042DD0000000000001 /* SileroCoreML in Frameworks */,
|
8C5000042DD0000000000001 /* SileroCoreML in Frameworks */,
|
||||||
1E3B5BCCA481234F14E64D44 /* Pods_Runner.framework in Frameworks */,
|
1E3B5BCCA481234F14E64D44 /* Pods_Runner.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
@@ -131,7 +128,6 @@
|
|||||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
@@ -220,7 +216,6 @@
|
|||||||
);
|
);
|
||||||
name = Runner;
|
name = Runner;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
|
||||||
8C5000032DD0000000000001 /* SileroCoreML */,
|
8C5000032DD0000000000001 /* SileroCoreML */,
|
||||||
);
|
);
|
||||||
productName = Runner;
|
productName = Runner;
|
||||||
@@ -257,7 +252,6 @@
|
|||||||
);
|
);
|
||||||
mainGroup = 97C146E51CF9000F007C117D;
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
|
||||||
8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */,
|
8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
@@ -766,10 +760,6 @@
|
|||||||
isa = XCLocalSwiftPackageReference;
|
isa = XCLocalSwiftPackageReference;
|
||||||
relativePath = ../../../silero-coreml;
|
relativePath = ../../../silero-coreml;
|
||||||
};
|
};
|
||||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
|
||||||
isa = XCLocalSwiftPackageReference;
|
|
||||||
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
|
||||||
};
|
|
||||||
/* End XCLocalSwiftPackageReference section */
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
@@ -778,10 +768,6 @@
|
|||||||
package = 8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */;
|
package = 8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */;
|
||||||
productName = SileroCoreML;
|
productName = SileroCoreML;
|
||||||
};
|
};
|
||||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
productName = FlutterGeneratedPluginSwiftPackage;
|
|
||||||
};
|
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
|||||||
@@ -5,24 +5,6 @@
|
|||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES">
|
buildImplicitDependencies = "YES">
|
||||||
<PreActions>
|
|
||||||
<ExecutionAction
|
|
||||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
|
||||||
<ActionContent
|
|
||||||
title = "Run Prepare Flutter Framework Script"
|
|
||||||
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
|
||||||
<EnvironmentBuildable>
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
|
||||||
BuildableName = "Runner.app"
|
|
||||||
BlueprintName = "Runner"
|
|
||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</EnvironmentBuildable>
|
|
||||||
</ActionContent>
|
|
||||||
</ExecutionAction>
|
|
||||||
</PreActions>
|
|
||||||
<BuildActionEntries>
|
<BuildActionEntries>
|
||||||
<BuildActionEntry
|
<BuildActionEntry
|
||||||
buildForTesting = "YES"
|
buildForTesting = "YES"
|
||||||
|
|||||||
@@ -298,6 +298,5 @@
|
|||||||
"clientVolumeMuteAction": "Mute user",
|
"clientVolumeMuteAction": "Mute user",
|
||||||
"clientVolumeUnmuteAction": "Unmute user",
|
"clientVolumeUnmuteAction": "Unmute user",
|
||||||
"clientVolumeResetAction": "Reset to default",
|
"clientVolumeResetAction": "Reset to default",
|
||||||
"permissionDenied": "Permission Denied",
|
"permissionDenied": "Permission Denied"
|
||||||
"voiceTalkPowerBlocked": "Insufficient talk power to speak in this channel"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,6 +241,5 @@
|
|||||||
"clientVolumeMuteAction": "静音该用户",
|
"clientVolumeMuteAction": "静音该用户",
|
||||||
"clientVolumeUnmuteAction": "取消静音",
|
"clientVolumeUnmuteAction": "取消静音",
|
||||||
"clientVolumeResetAction": "恢复默认",
|
"clientVolumeResetAction": "恢复默认",
|
||||||
"permissionDenied": "权限被拒绝",
|
"permissionDenied": "权限被拒绝"
|
||||||
"voiceTalkPowerBlocked": "发言权限不足,无法在此频道发言"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1246,12 +1246,6 @@ abstract class AppL10n {
|
|||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Permission Denied'**
|
/// **'Permission Denied'**
|
||||||
String get permissionDenied;
|
String get permissionDenied;
|
||||||
|
|
||||||
/// No description provided for @voiceTalkPowerBlocked.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Insufficient talk power to speak in this channel'**
|
|
||||||
String get voiceTalkPowerBlocked;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppL10nDelegate extends LocalizationsDelegate<AppL10n> {
|
class _AppL10nDelegate extends LocalizationsDelegate<AppL10n> {
|
||||||
|
|||||||
@@ -653,8 +653,4 @@ class AppL10nEn extends AppL10n {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get permissionDenied => 'Permission Denied';
|
String get permissionDenied => 'Permission Denied';
|
||||||
|
|
||||||
@override
|
|
||||||
String get voiceTalkPowerBlocked =>
|
|
||||||
'Insufficient talk power to speak in this channel';
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -641,7 +641,4 @@ class AppL10nZh extends AppL10n {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get permissionDenied => '权限被拒绝';
|
String get permissionDenied => '权限被拒绝';
|
||||||
|
|
||||||
@override
|
|
||||||
String get voiceTalkPowerBlocked => '发言权限不足,无法在此频道发言';
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,7 +312,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
rust.BridgeSnapshot? _snapshot;
|
rust.BridgeSnapshot? _snapshot;
|
||||||
final List<String> _uiDiagnostics = [];
|
final List<String> _uiDiagnostics = [];
|
||||||
rust.BridgeAudioStats? _audioStats;
|
rust.BridgeAudioStats? _audioStats;
|
||||||
double? _inputLevel;
|
|
||||||
Timer? _statsTimer;
|
Timer? _statsTimer;
|
||||||
bool _snapshotRefreshInFlight = false;
|
bool _snapshotRefreshInFlight = false;
|
||||||
bool _snapshotRefreshQueued = false;
|
bool _snapshotRefreshQueued = false;
|
||||||
@@ -320,7 +319,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
bool _snapshotRefreshQueuedReportErrors = false;
|
bool _snapshotRefreshQueuedReportErrors = false;
|
||||||
int _connectionEpoch = 0;
|
int _connectionEpoch = 0;
|
||||||
StreamSubscription<rust.BridgeEvent>? _eventsSub;
|
StreamSubscription<rust.BridgeEvent>? _eventsSub;
|
||||||
StreamSubscription<double>? _inputLevelSub;
|
|
||||||
late final PrefetchDebouncer _prefetch;
|
late final PrefetchDebouncer _prefetch;
|
||||||
|
|
||||||
// v1 voice subsystem state (SDD-094/095/096/097). Driven by
|
// v1 voice subsystem state (SDD-094/095/096/097). Driven by
|
||||||
@@ -385,7 +383,8 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
// and Notifications permission service. On non-macOS hosts the service
|
// and Notifications permission service. On non-macOS hosts the service
|
||||||
// short-circuits to "granted" / "unsupported" and never wires the
|
// short-circuits to "granted" / "unsupported" and never wires the
|
||||||
// MethodChannel.
|
// MethodChannel.
|
||||||
final MacOSPermissionsService _macOSPermissions = MacOSPermissionsService();
|
final MacOSPermissionsService _macOSPermissions =
|
||||||
|
MacOSPermissionsService();
|
||||||
final UiPreferencesService _uiPreferences = const UiPreferencesService();
|
final UiPreferencesService _uiPreferences = const UiPreferencesService();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -396,9 +395,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
_prefetch = PrefetchDebouncer(onPrefetch: _onPrefetchServer);
|
_prefetch = PrefetchDebouncer(onPrefetch: _onPrefetchServer);
|
||||||
_hostCtl.addListener(_onHostEdited);
|
_hostCtl.addListener(_onHostEdited);
|
||||||
_eventsSub = rust.eventsStream().listen(_onEvent);
|
_eventsSub = rust.eventsStream().listen(_onEvent);
|
||||||
_inputLevelSub = rust.inputLevelStream().listen((level) {
|
|
||||||
if (mounted) setState(() => _inputLevel = level);
|
|
||||||
});
|
|
||||||
// SDD-106 §5: subscribe to Kotlin -> Dart permissionStateChanged
|
// SDD-106 §5: subscribe to Kotlin -> Dart permissionStateChanged
|
||||||
// events as early as possible so the listen-only banner reflects
|
// events as early as possible so the listen-only banner reflects
|
||||||
// the system state on first frame.
|
// the system state on first frame.
|
||||||
@@ -416,10 +412,10 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
// capability badge upgrades from L0Focused → L1MacOSEventTap
|
// capability badge upgrades from L0Focused → L1MacOSEventTap
|
||||||
// when the user grants the permission in System Settings.
|
// when the user grants the permission in System Settings.
|
||||||
_macOSPermissions.start();
|
_macOSPermissions.start();
|
||||||
|
_macOSPermissions.checkInitialStates();
|
||||||
_macOSPermissions.pttCapabilityState.addListener(
|
_macOSPermissions.pttCapabilityState.addListener(
|
||||||
_onMacOSPttCapabilityChanged,
|
_onMacOSPttCapabilityChanged,
|
||||||
);
|
);
|
||||||
_macOSPermissions.checkInitialStates();
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
unawaited(_requestRecordAudioOnStartup());
|
unawaited(_requestRecordAudioOnStartup());
|
||||||
});
|
});
|
||||||
@@ -534,8 +530,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
_pttLevel = level;
|
_pttLevel = level;
|
||||||
if (level == 'L1MacOSEventTap') {
|
if (level == 'L1MacOSEventTap') {
|
||||||
_pttBackendId = 'macos-event-tap';
|
_pttBackendId = 'macos-event-tap';
|
||||||
} else if (level == 'L0Focused') {
|
|
||||||
_pttBackendId = 'focused';
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -676,10 +670,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
_reconnectDelay = null;
|
_reconnectDelay = null;
|
||||||
});
|
});
|
||||||
case rust.BridgeEvent_Reconnecting(:final attempt, :final delaySecs):
|
case rust.BridgeEvent_Reconnecting(:final attempt, :final delaySecs):
|
||||||
_recordUiDiagnostic(
|
_recordUiDiagnostic('connection', 'reconnecting attempt=$attempt delay=${delaySecs}s');
|
||||||
'connection',
|
|
||||||
'reconnecting attempt=$attempt delay=${delaySecs}s',
|
|
||||||
);
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_phase = ConnectionPhase.reconnecting;
|
_phase = ConnectionPhase.reconnecting;
|
||||||
_reconnectAttempt = attempt;
|
_reconnectAttempt = attempt;
|
||||||
@@ -854,19 +845,9 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
);
|
);
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
case rust.BridgeEvent_ClientJoined(
|
case rust.BridgeEvent_ClientJoined(:final clientId, :final channelId, :final name, :final inputMuted, :final outputMuted, :final isServerQuery, :final talkPower, :final talkPowerGranted):
|
||||||
:final clientId,
|
|
||||||
:final channelId,
|
|
||||||
:final name,
|
|
||||||
:final inputMuted,
|
|
||||||
:final outputMuted,
|
|
||||||
:final isServerQuery,
|
|
||||||
:final talkPower,
|
|
||||||
:final talkPowerGranted,
|
|
||||||
):
|
|
||||||
if (!isServerQuery) {
|
if (!isServerQuery) {
|
||||||
_applyClientAdd(
|
_applyClientAdd(rust.BridgeClient(
|
||||||
rust.BridgeClient(
|
|
||||||
id: clientId,
|
id: clientId,
|
||||||
channel: channelId,
|
channel: channelId,
|
||||||
name: name,
|
name: name,
|
||||||
@@ -876,19 +857,11 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
isServerQuery: isServerQuery,
|
isServerQuery: isServerQuery,
|
||||||
talkPower: talkPower,
|
talkPower: talkPower,
|
||||||
talkPowerGranted: talkPowerGranted,
|
talkPowerGranted: talkPowerGranted,
|
||||||
),
|
));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
case rust.BridgeEvent_ClientLeft(:final clientId):
|
case rust.BridgeEvent_ClientLeft(:final clientId):
|
||||||
_applyClientRemove(clientId);
|
_applyClientRemove(clientId);
|
||||||
case rust.BridgeEvent_ClientUpdated(
|
case rust.BridgeEvent_ClientUpdated(:final clientId, :final inputMuted, :final outputMuted, :final isServerQuery, :final talkPower, :final talkPowerGranted):
|
||||||
:final clientId,
|
|
||||||
:final inputMuted,
|
|
||||||
:final outputMuted,
|
|
||||||
:final isServerQuery,
|
|
||||||
:final talkPower,
|
|
||||||
:final talkPowerGranted,
|
|
||||||
):
|
|
||||||
_applyClientDelta((c) => c.id == clientId, (c) {
|
_applyClientDelta((c) => c.id == clientId, (c) {
|
||||||
final updated = rust.BridgeClient(
|
final updated = rust.BridgeClient(
|
||||||
id: c.id,
|
id: c.id,
|
||||||
@@ -903,32 +876,18 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
);
|
);
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
case rust.BridgeEvent_ChannelAdded(
|
case rust.BridgeEvent_ChannelAdded(:final id, :final parent, :final name, :final order, :final hasPassword, :final neededTalkPower):
|
||||||
:final id,
|
_applyChannelAdd(rust.BridgeChannel(
|
||||||
:final parent,
|
|
||||||
:final name,
|
|
||||||
:final order,
|
|
||||||
:final hasPassword,
|
|
||||||
:final neededTalkPower,
|
|
||||||
):
|
|
||||||
_applyChannelAdd(
|
|
||||||
rust.BridgeChannel(
|
|
||||||
id: id,
|
id: id,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
name: name,
|
name: name,
|
||||||
order: order,
|
order: order,
|
||||||
hasPassword: hasPassword,
|
hasPassword: hasPassword,
|
||||||
neededTalkPower: neededTalkPower,
|
neededTalkPower: neededTalkPower,
|
||||||
),
|
));
|
||||||
);
|
|
||||||
case rust.BridgeEvent_ChannelRemoved(:final id):
|
case rust.BridgeEvent_ChannelRemoved(:final id):
|
||||||
_applyChannelRemove(id);
|
_applyChannelRemove(id);
|
||||||
case rust.BridgeEvent_ChannelUpdated(
|
case rust.BridgeEvent_ChannelUpdated(:final id, :final name, :final hasPassword, :final neededTalkPower):
|
||||||
:final id,
|
|
||||||
:final name,
|
|
||||||
:final hasPassword,
|
|
||||||
:final neededTalkPower,
|
|
||||||
):
|
|
||||||
_applyChannelDelta((ch) => ch.id == id, (ch) {
|
_applyChannelDelta((ch) => ch.id == id, (ch) {
|
||||||
return rust.BridgeChannel(
|
return rust.BridgeChannel(
|
||||||
id: ch.id,
|
id: ch.id,
|
||||||
@@ -966,8 +925,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
if (_inChannel &&
|
if (_inChannel &&
|
||||||
(_lastSpeakingRefresh == null ||
|
(_lastSpeakingRefresh == null ||
|
||||||
now.difference(_lastSpeakingRefresh!) >=
|
now.difference(_lastSpeakingRefresh!) >= _speakingRefreshInterval)) {
|
||||||
_speakingRefreshInterval)) {
|
|
||||||
_lastSpeakingRefresh = now;
|
_lastSpeakingRefresh = now;
|
||||||
unawaited(_refreshSnapshot(recordActivity: false, reportErrors: false));
|
unawaited(_refreshSnapshot(recordActivity: false, reportErrors: false));
|
||||||
}
|
}
|
||||||
@@ -997,7 +955,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
HardwareKeyboard.instance.removeHandler(_handleFocusedPttKey);
|
HardwareKeyboard.instance.removeHandler(_handleFocusedPttKey);
|
||||||
_eventsSub?.cancel();
|
_eventsSub?.cancel();
|
||||||
_inputLevelSub?.cancel();
|
|
||||||
_statsTimer?.cancel();
|
_statsTimer?.cancel();
|
||||||
_snapshotRefreshInFlight = false;
|
_snapshotRefreshInFlight = false;
|
||||||
_snapshotRefreshQueued = false;
|
_snapshotRefreshQueued = false;
|
||||||
@@ -1169,6 +1126,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
Future<void> _toggleOutputMute() async {
|
Future<void> _toggleOutputMute() async {
|
||||||
final next = !_outputMuted;
|
final next = !_outputMuted;
|
||||||
|
// Optimistic: flip the UI immediately.
|
||||||
setState(() {
|
setState(() {
|
||||||
_outputMuted = next;
|
_outputMuted = next;
|
||||||
});
|
});
|
||||||
@@ -1176,6 +1134,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
await rust.setOutputMuted(muted: next);
|
await rust.setOutputMuted(muted: next);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
// Roll back on failure.
|
||||||
setState(() {
|
setState(() {
|
||||||
_outputMuted = !next;
|
_outputMuted = !next;
|
||||||
});
|
});
|
||||||
@@ -1305,15 +1264,12 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final next = !_hardMute;
|
final next = !_hardMute;
|
||||||
final previousInputMuted = _inputMuted;
|
// Optimistic: flip the UI immediately so the icon responds
|
||||||
final previousHardMute = _hardMute;
|
// before the two bridge calls round-trip through FFI.
|
||||||
final previousPermissionMute = _hardMuteByPermission;
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_inputMuted = next;
|
_inputMuted = next;
|
||||||
_hardMute = next;
|
_hardMute = next;
|
||||||
if (next) {
|
|
||||||
_hardMuteByPermission = false;
|
_hardMuteByPermission = false;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
// Hard-mute is two coordinated effects:
|
// Hard-mute is two coordinated effects:
|
||||||
@@ -1329,10 +1285,10 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
await rust.setInputMuted(muted: next);
|
await rust.setInputMuted(muted: next);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
// Roll back on failure.
|
||||||
setState(() {
|
setState(() {
|
||||||
_inputMuted = previousInputMuted;
|
_inputMuted = !next;
|
||||||
_hardMute = previousHardMute;
|
_hardMute = !next;
|
||||||
_hardMuteByPermission = previousPermissionMute;
|
|
||||||
});
|
});
|
||||||
_showUiError('hard mute', e);
|
_showUiError('hard mute', e);
|
||||||
}
|
}
|
||||||
@@ -1791,10 +1747,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void _applyClientDelta(
|
void _applyClientDelta(bool Function(rust.BridgeClient) test, rust.BridgeClient Function(rust.BridgeClient) update) {
|
||||||
bool Function(rust.BridgeClient) test,
|
|
||||||
rust.BridgeClient Function(rust.BridgeClient) update,
|
|
||||||
) {
|
|
||||||
final snap = _snapshot;
|
final snap = _snapshot;
|
||||||
if (snap == null) return;
|
if (snap == null) return;
|
||||||
final clients = snap.clients.map((c) => test(c) ? update(c) : c).toList();
|
final clients = snap.clients.map((c) => test(c) ? update(c) : c).toList();
|
||||||
@@ -1880,15 +1833,10 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _applyChannelDelta(
|
void _applyChannelDelta(bool Function(rust.BridgeChannel) test, rust.BridgeChannel Function(rust.BridgeChannel) update) {
|
||||||
bool Function(rust.BridgeChannel) test,
|
|
||||||
rust.BridgeChannel Function(rust.BridgeChannel) update,
|
|
||||||
) {
|
|
||||||
final snap = _snapshot;
|
final snap = _snapshot;
|
||||||
if (snap == null) return;
|
if (snap == null) return;
|
||||||
final channels = snap.channels
|
final channels = snap.channels.map((ch) => test(ch) ? update(ch) : ch).toList();
|
||||||
.map((ch) => test(ch) ? update(ch) : ch)
|
|
||||||
.toList();
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_snapshot = rust.BridgeSnapshot(
|
_snapshot = rust.BridgeSnapshot(
|
||||||
serverName: snap.serverName,
|
serverName: snap.serverName,
|
||||||
@@ -2418,7 +2366,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
_currentVoiceChannelId,
|
_currentVoiceChannelId,
|
||||||
),
|
),
|
||||||
audioStats: _audioStats,
|
audioStats: _audioStats,
|
||||||
inputLevel: _inputLevel,
|
|
||||||
pttLevel: _pttLevel,
|
pttLevel: _pttLevel,
|
||||||
pttBackendId: _pttBackendId,
|
pttBackendId: _pttBackendId,
|
||||||
pttBoundInputClass: _pttBoundInputClass,
|
pttBoundInputClass: _pttBoundInputClass,
|
||||||
@@ -2492,7 +2439,8 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
Expanded(child: snapshotView),
|
Expanded(child: snapshotView),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
permissionBanner,
|
permissionBanner,
|
||||||
VoiceStatusChip(
|
CompactVoiceBar(
|
||||||
|
inChannel: _inChannel,
|
||||||
transmitMode: _transmitMode,
|
transmitMode: _transmitMode,
|
||||||
releaseTailMs: _releaseTailMs,
|
releaseTailMs: _releaseTailMs,
|
||||||
pttBoundKeyLabel: _pttBoundKeyLabel,
|
pttBoundKeyLabel: _pttBoundKeyLabel,
|
||||||
@@ -2500,22 +2448,15 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
isTouchOnly: isTouchOnlyPttHost,
|
isTouchOnly: isTouchOnlyPttHost,
|
||||||
inputMuted: _hardMute,
|
inputMuted: _hardMute,
|
||||||
outputMuted: _outputMuted,
|
outputMuted: _outputMuted,
|
||||||
hardMuteByTalkPower: _hardMuteByTalkPower,
|
onToggleInputMute: _onToggleHardMute,
|
||||||
|
onToggleOutputMute: _toggleOutputMute,
|
||||||
|
onOpenDetails: () => _onOpenVoiceDetailsSheet(),
|
||||||
|
onPttHeldChanged: _onOnscreenPttHeldChanged,
|
||||||
talkPower: ownClientState?.talkPower,
|
talkPower: ownClientState?.talkPower,
|
||||||
neededTalkPower: ownClientState?.neededTalkPower,
|
neededTalkPower: ownClientState?.neededTalkPower,
|
||||||
talkPowerGranted: ownClientState?.talkPowerGranted,
|
talkPowerGranted: ownClientState?.talkPowerGranted,
|
||||||
onTap: () => _onOpenVoiceDetailsSheet(),
|
hardMuteByTalkPower: _hardMuteByTalkPower,
|
||||||
onToggleInputMute: _onToggleHardMute,
|
|
||||||
onToggleOutputMute: _toggleOutputMute,
|
|
||||||
),
|
),
|
||||||
if (_inChannel &&
|
|
||||||
_transmitMode == rust.BridgeTransmitMode.ptt) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
VoicePttButton(
|
|
||||||
active: _audioStats?.pttActive ?? false,
|
|
||||||
onHeldChanged: _onOnscreenPttHeldChanged,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -309,14 +309,12 @@ class MacOSPermissionsService {
|
|||||||
_inputMonitoringState.value = state;
|
_inputMonitoringState.value = state;
|
||||||
_pttCapabilityState.value = _pttCapabilityLevel(state);
|
_pttCapabilityState.value = _pttCapabilityLevel(state);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case methodLocalNetworkStateChanged:
|
case methodLocalNetworkStateChanged:
|
||||||
final args = call.arguments;
|
final args = call.arguments;
|
||||||
if (args is Map) {
|
if (args is Map) {
|
||||||
_localNetworkState.value =
|
_localNetworkState.value =
|
||||||
_parseLocalNetworkState(args['state'] as String?);
|
_parseLocalNetworkState(args['state'] as String?);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -261,12 +261,6 @@ Stream<BridgeEvent> eventsStream() =>
|
|||||||
Future<BridgeAudioStats> audioStats() =>
|
Future<BridgeAudioStats> audioStats() =>
|
||||||
RustLib.instance.api.crateApiAudioStats();
|
RustLib.instance.api.crateApiAudioStats();
|
||||||
|
|
||||||
/// Subscribe to real-time microphone input level at ~30 Hz.
|
|
||||||
/// Values are dBFS (-120 = silence, 0 = clipping). The stream ends
|
|
||||||
/// when the Dart subscriber cancels or the session is dropped.
|
|
||||||
Stream<double> inputLevelStream() =>
|
|
||||||
RustLib.instance.api.crateApiInputLevelStream();
|
|
||||||
|
|
||||||
/// Apply the P1 audio-processing config.
|
/// Apply the P1 audio-processing config.
|
||||||
Future<void> setAudioProcessingConfig({
|
Future<void> setAudioProcessingConfig({
|
||||||
required BridgeAudioProcessingConfig config,
|
required BridgeAudioProcessingConfig config,
|
||||||
@@ -687,22 +681,15 @@ class BridgeAudioStats {
|
|||||||
/// Current push-to-talk state.
|
/// Current push-to-talk state.
|
||||||
final bool pttActive;
|
final bool pttActive;
|
||||||
|
|
||||||
/// Current microphone input level in dBFS (-120.0 = silence, 0.0 = clipping).
|
|
||||||
final double inputLevel;
|
|
||||||
|
|
||||||
const BridgeAudioStats({
|
const BridgeAudioStats({
|
||||||
required this.framesSent,
|
required this.framesSent,
|
||||||
required this.framesReceived,
|
required this.framesReceived,
|
||||||
required this.pttActive,
|
required this.pttActive,
|
||||||
required this.inputLevel,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
framesSent.hashCode ^
|
framesSent.hashCode ^ framesReceived.hashCode ^ pttActive.hashCode;
|
||||||
framesReceived.hashCode ^
|
|
||||||
pttActive.hashCode ^
|
|
||||||
inputLevel.hashCode;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
@@ -711,8 +698,7 @@ class BridgeAudioStats {
|
|||||||
runtimeType == other.runtimeType &&
|
runtimeType == other.runtimeType &&
|
||||||
framesSent == other.framesSent &&
|
framesSent == other.framesSent &&
|
||||||
framesReceived == other.framesReceived &&
|
framesReceived == other.framesReceived &&
|
||||||
pttActive == other.pttActive &&
|
pttActive == other.pttActive;
|
||||||
inputLevel == other.inputLevel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bookmark DTO mirroring [`chanora_core::Bookmark`].
|
/// Bookmark DTO mirroring [`chanora_core::Bookmark`].
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
|||||||
String get codegenVersion => '2.12.0';
|
String get codegenVersion => '2.12.0';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get rustContentHash => -20394775;
|
int get rustContentHash => 281698435;
|
||||||
|
|
||||||
static const kDefaultExternalLibraryLoaderConfig =
|
static const kDefaultExternalLibraryLoaderConfig =
|
||||||
ExternalLibraryLoaderConfig(
|
ExternalLibraryLoaderConfig(
|
||||||
@@ -123,8 +123,6 @@ abstract class RustLibApi extends BaseApi {
|
|||||||
|
|
||||||
Future<void> crateApiInitStorage({required String dir});
|
Future<void> crateApiInitStorage({required String dir});
|
||||||
|
|
||||||
Stream<double> crateApiInputLevelStream();
|
|
||||||
|
|
||||||
Future<bool> crateApiIsConnected();
|
Future<bool> crateApiIsConnected();
|
||||||
|
|
||||||
Future<BridgeAudioDeviceList> crateApiListAudioDevices();
|
Future<BridgeAudioDeviceList> crateApiListAudioDevices();
|
||||||
@@ -764,38 +762,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
TaskConstMeta get kCrateApiInitStorageConstMeta =>
|
TaskConstMeta get kCrateApiInitStorageConstMeta =>
|
||||||
const TaskConstMeta(debugName: "init_storage", argNames: ["dir"]);
|
const TaskConstMeta(debugName: "init_storage", argNames: ["dir"]);
|
||||||
|
|
||||||
@override
|
|
||||||
Stream<double> crateApiInputLevelStream() {
|
|
||||||
final sink = RustStreamSink<double>();
|
|
||||||
unawaited(
|
|
||||||
handler.executeNormal(
|
|
||||||
NormalTask(
|
|
||||||
callFfi: (port_) {
|
|
||||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
|
||||||
sse_encode_StreamSink_f_32_Sse(sink, serializer);
|
|
||||||
pdeCallFfi(
|
|
||||||
generalizedFrbRustBinding,
|
|
||||||
serializer,
|
|
||||||
funcId: 21,
|
|
||||||
port: port_,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
codec: SseCodec(
|
|
||||||
decodeSuccessData: sse_decode_unit,
|
|
||||||
decodeErrorData: sse_decode_bridge_error,
|
|
||||||
),
|
|
||||||
constMeta: kCrateApiInputLevelStreamConstMeta,
|
|
||||||
argValues: [sink],
|
|
||||||
apiImpl: this,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return sink.stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskConstMeta get kCrateApiInputLevelStreamConstMeta =>
|
|
||||||
const TaskConstMeta(debugName: "input_level_stream", argNames: ["sink"]);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> crateApiIsConnected() {
|
Future<bool> crateApiIsConnected() {
|
||||||
return handler.executeNormal(
|
return handler.executeNormal(
|
||||||
@@ -805,7 +771,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 22,
|
funcId: 21,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -832,7 +798,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 23,
|
funcId: 22,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -859,7 +825,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 24,
|
funcId: 23,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -883,7 +849,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
SyncTask(
|
SyncTask(
|
||||||
callFfi: () {
|
callFfi: () {
|
||||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25)!;
|
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 24)!;
|
||||||
},
|
},
|
||||||
codec: SseCodec(
|
codec: SseCodec(
|
||||||
decodeSuccessData: sse_decode_String,
|
decodeSuccessData: sse_decode_String,
|
||||||
@@ -913,7 +879,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 26,
|
funcId: 25,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -943,7 +909,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 27,
|
funcId: 26,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -970,7 +936,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 28,
|
funcId: 27,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -995,7 +961,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
callFfi: () {
|
callFfi: () {
|
||||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
sse_encode_String(state, serializer);
|
sse_encode_String(state, serializer);
|
||||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 29)!;
|
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 28)!;
|
||||||
},
|
},
|
||||||
codec: SseCodec(
|
codec: SseCodec(
|
||||||
decodeSuccessData: sse_decode_unit,
|
decodeSuccessData: sse_decode_unit,
|
||||||
@@ -1028,7 +994,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 30,
|
funcId: 29,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1055,7 +1021,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
callFfi: () {
|
callFfi: () {
|
||||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
sse_encode_bridge_audio_route(route, serializer);
|
sse_encode_bridge_audio_route(route, serializer);
|
||||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 31)!;
|
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30)!;
|
||||||
},
|
},
|
||||||
codec: SseCodec(
|
codec: SseCodec(
|
||||||
decodeSuccessData: sse_decode_unit,
|
decodeSuccessData: sse_decode_unit,
|
||||||
@@ -1089,7 +1055,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 32,
|
funcId: 31,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1124,7 +1090,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 33,
|
funcId: 32,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1154,7 +1120,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 34,
|
funcId: 33,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1182,7 +1148,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 35,
|
funcId: 34,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1210,7 +1176,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 36,
|
funcId: 35,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1240,7 +1206,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 37,
|
funcId: 36,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1268,7 +1234,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
callFfi: () {
|
callFfi: () {
|
||||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
sse_encode_bridge_network_state(state, serializer);
|
sse_encode_bridge_network_state(state, serializer);
|
||||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 38)!;
|
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 37)!;
|
||||||
},
|
},
|
||||||
codec: SseCodec(
|
codec: SseCodec(
|
||||||
decodeSuccessData: sse_decode_unit,
|
decodeSuccessData: sse_decode_unit,
|
||||||
@@ -1294,7 +1260,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 39,
|
funcId: 38,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1322,7 +1288,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 40,
|
funcId: 39,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1350,7 +1316,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 41,
|
funcId: 40,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1378,7 +1344,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 42,
|
funcId: 41,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1410,7 +1376,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 43,
|
funcId: 42,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1440,7 +1406,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 44,
|
funcId: 43,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1468,7 +1434,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 45,
|
funcId: 44,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1496,7 +1462,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 46,
|
funcId: 45,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1523,7 +1489,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 47,
|
funcId: 46,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1551,7 +1517,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 48,
|
funcId: 47,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1583,7 +1549,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 49,
|
funcId: 48,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1612,7 +1578,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 50,
|
funcId: 49,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1644,12 +1610,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
|
||||||
RustStreamSink<double> dco_decode_StreamSink_f_32_Sse(dynamic raw) {
|
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String dco_decode_String(dynamic raw) {
|
String dco_decode_String(dynamic raw) {
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
@@ -1821,13 +1781,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
BridgeAudioStats dco_decode_bridge_audio_stats(dynamic raw) {
|
BridgeAudioStats dco_decode_bridge_audio_stats(dynamic raw) {
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
final arr = raw as List<dynamic>;
|
final arr = raw as List<dynamic>;
|
||||||
if (arr.length != 4)
|
if (arr.length != 3)
|
||||||
throw Exception('unexpected arr length: expect 4 but see ${arr.length}');
|
throw Exception('unexpected arr length: expect 3 but see ${arr.length}');
|
||||||
return BridgeAudioStats(
|
return BridgeAudioStats(
|
||||||
framesSent: dco_decode_u_32(arr[0]),
|
framesSent: dco_decode_u_32(arr[0]),
|
||||||
framesReceived: dco_decode_u_32(arr[1]),
|
framesReceived: dco_decode_u_32(arr[1]),
|
||||||
pttActive: dco_decode_bool(arr[2]),
|
pttActive: dco_decode_bool(arr[2]),
|
||||||
inputLevel: dco_decode_f_32(arr[3]),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2312,14 +2271,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
throw UnimplementedError('Unreachable ()');
|
throw UnimplementedError('Unreachable ()');
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
|
||||||
RustStreamSink<double> sse_decode_StreamSink_f_32_Sse(
|
|
||||||
SseDeserializer deserializer,
|
|
||||||
) {
|
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
|
||||||
throw UnimplementedError('Unreachable ()');
|
|
||||||
}
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String sse_decode_String(SseDeserializer deserializer) {
|
String sse_decode_String(SseDeserializer deserializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
@@ -2537,12 +2488,10 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
var var_framesSent = sse_decode_u_32(deserializer);
|
var var_framesSent = sse_decode_u_32(deserializer);
|
||||||
var var_framesReceived = sse_decode_u_32(deserializer);
|
var var_framesReceived = sse_decode_u_32(deserializer);
|
||||||
var var_pttActive = sse_decode_bool(deserializer);
|
var var_pttActive = sse_decode_bool(deserializer);
|
||||||
var var_inputLevel = sse_decode_f_32(deserializer);
|
|
||||||
return BridgeAudioStats(
|
return BridgeAudioStats(
|
||||||
framesSent: var_framesSent,
|
framesSent: var_framesSent,
|
||||||
framesReceived: var_framesReceived,
|
framesReceived: var_framesReceived,
|
||||||
pttActive: var_pttActive,
|
pttActive: var_pttActive,
|
||||||
inputLevel: var_inputLevel,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3250,23 +3199,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_StreamSink_f_32_Sse(
|
|
||||||
RustStreamSink<double> self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
) {
|
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
|
||||||
sse_encode_String(
|
|
||||||
self.setupAndSerialize(
|
|
||||||
codec: SseCodec(
|
|
||||||
decodeSuccessData: sse_decode_f_32,
|
|
||||||
decodeErrorData: sse_decode_AnyhowException,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
serializer,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_String(String self, SseSerializer serializer) {
|
void sse_encode_String(String self, SseSerializer serializer) {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
@@ -3448,7 +3380,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
sse_encode_u_32(self.framesSent, serializer);
|
sse_encode_u_32(self.framesSent, serializer);
|
||||||
sse_encode_u_32(self.framesReceived, serializer);
|
sse_encode_u_32(self.framesReceived, serializer);
|
||||||
sse_encode_bool(self.pttActive, serializer);
|
sse_encode_bool(self.pttActive, serializer);
|
||||||
sse_encode_f_32(self.inputLevel, serializer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
dynamic raw,
|
dynamic raw,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
RustStreamSink<double> dco_decode_StreamSink_f_32_Sse(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String dco_decode_String(dynamic raw);
|
String dco_decode_String(dynamic raw);
|
||||||
|
|
||||||
@@ -213,11 +210,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
RustStreamSink<double> sse_decode_StreamSink_f_32_Sse(
|
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String sse_decode_String(SseDeserializer deserializer);
|
String sse_decode_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
@@ -447,12 +439,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_StreamSink_f_32_Sse(
|
|
||||||
RustStreamSink<double> self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_String(String self, SseSerializer serializer);
|
void sse_encode_String(String self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
dynamic raw,
|
dynamic raw,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
RustStreamSink<double> dco_decode_StreamSink_f_32_Sse(dynamic raw);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String dco_decode_String(dynamic raw);
|
String dco_decode_String(dynamic raw);
|
||||||
|
|
||||||
@@ -215,11 +212,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
RustStreamSink<double> sse_decode_StreamSink_f_32_Sse(
|
|
||||||
SseDeserializer deserializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
String sse_decode_String(SseDeserializer deserializer);
|
String sse_decode_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
@@ -449,12 +441,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
@protected
|
|
||||||
void sse_encode_StreamSink_f_32_Sse(
|
|
||||||
RustStreamSink<double> self,
|
|
||||||
SseSerializer serializer,
|
|
||||||
);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_String(String self, SseSerializer serializer);
|
void sse_encode_String(String self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ class VoiceBar extends StatelessWidget {
|
|||||||
required this.onConfigure,
|
required this.onConfigure,
|
||||||
required this.onPttHeldChanged,
|
required this.onPttHeldChanged,
|
||||||
this.talkPowerBlocked = false,
|
this.talkPowerBlocked = false,
|
||||||
this.inputLevel,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final bool inChannel;
|
final bool inChannel;
|
||||||
@@ -54,10 +53,6 @@ class VoiceBar extends StatelessWidget {
|
|||||||
/// level meter. Pass `null` to render an idle meter.
|
/// level meter. Pass `null` to render an idle meter.
|
||||||
final rust.BridgeAudioStats? audioStats;
|
final rust.BridgeAudioStats? audioStats;
|
||||||
|
|
||||||
/// Real-time input level from the 30 Hz stream (dBFS).
|
|
||||||
/// When non-null, takes precedence over `audioStats.inputLevel`.
|
|
||||||
final double? inputLevel;
|
|
||||||
|
|
||||||
/// PTT capability badge inputs — passed through to
|
/// PTT capability badge inputs — passed through to
|
||||||
/// [`PttCapabilityBadge`].
|
/// [`PttCapabilityBadge`].
|
||||||
final String pttLevel;
|
final String pttLevel;
|
||||||
@@ -201,7 +196,7 @@ class VoiceBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
// Row 4: level meter
|
// Row 4: level meter
|
||||||
VoiceLevelMeter(active: levelActive, level: inputLevel ?? stats?.inputLevel),
|
VoiceLevelMeter(active: levelActive),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
if (stats != null)
|
if (stats != null)
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
// Compact voice UI for narrow / mobile layouts (Plan E hybrid:
|
// Compact voice UI for narrow / mobile layouts.
|
||||||
// AppBar mutes + status chip with 2-line live readout + wide bottom-
|
|
||||||
// anchored PTT button + modal sheet for non-essential controls).
|
|
||||||
//
|
//
|
||||||
// rc.8 follow-up: the AppBar gear icon was removed; the modal sheet
|
// Two-zone voice bar pinned to the bottom:
|
||||||
// is now the **single** voice-controls surface on mobile. Mode +
|
// • Control row: status text + mute + deafen + settings chevron
|
||||||
// release-tail are surfaced inline (radio buttons + slider) inside
|
// • PTT row: full-width hold-to-talk (PTT mode only)
|
||||||
// the modal.
|
// Both share a single container whose background colour reflects
|
||||||
|
// the current voice state (normal / muted / talk-power-blocked).
|
||||||
|
//
|
||||||
|
// Gesture isolation: the control row uses tap-only InkWell /
|
||||||
|
// IconButton; the PTT row uses a raw Listener for pointer-down /
|
||||||
|
// pointer-up. Because each row is a disjoint hit-test region, a
|
||||||
|
// finger holding PTT cannot accidentally toggle mute or deafen.
|
||||||
|
|
||||||
import 'dart:async' show StreamSubscription, Timer, unawaited;
|
import 'dart:async' show Timer, unawaited;
|
||||||
import 'dart:io' show Platform;
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
@@ -53,11 +57,8 @@ class VoiceStatusChip extends StatelessWidget {
|
|||||||
required this.audioStats,
|
required this.audioStats,
|
||||||
required this.isTouchOnly,
|
required this.isTouchOnly,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
required this.onToggleInputMute,
|
|
||||||
required this.onToggleOutputMute,
|
|
||||||
this.inputMuted = false,
|
this.inputMuted = false,
|
||||||
this.outputMuted = false,
|
this.outputMuted = false,
|
||||||
this.hardMuteByTalkPower = false,
|
|
||||||
this.talkPower,
|
this.talkPower,
|
||||||
this.neededTalkPower,
|
this.neededTalkPower,
|
||||||
this.talkPowerGranted,
|
this.talkPowerGranted,
|
||||||
@@ -84,9 +85,6 @@ class VoiceStatusChip extends StatelessWidget {
|
|||||||
/// True when local speaker is muted.
|
/// True when local speaker is muted.
|
||||||
final bool outputMuted;
|
final bool outputMuted;
|
||||||
|
|
||||||
/// True when the server talk-power gate forces local hard mute.
|
|
||||||
final bool hardMuteByTalkPower;
|
|
||||||
|
|
||||||
/// Own client's talk power.
|
/// Own client's talk power.
|
||||||
final int? talkPower;
|
final int? talkPower;
|
||||||
|
|
||||||
@@ -99,12 +97,6 @@ class VoiceStatusChip extends StatelessWidget {
|
|||||||
/// Open the voice details modal.
|
/// Open the voice details modal.
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
/// Toggle local input hard mute.
|
|
||||||
final VoidCallback onToggleInputMute;
|
|
||||||
|
|
||||||
/// Toggle local output mute/deafen.
|
|
||||||
final VoidCallback onToggleOutputMute;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
@@ -125,9 +117,18 @@ class VoiceStatusChip extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return Semantics(
|
return Semantics(
|
||||||
|
button: true,
|
||||||
label: '${l10n.voiceSheetTitle}: ${summary.line1}, ${summary.line2}',
|
label: '${l10n.voiceSheetTitle}: ${summary.line1}, ${summary.line2}',
|
||||||
|
hint: l10n.voiceSettingsTitle,
|
||||||
child: Material(
|
child: Material(
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
onTap();
|
||||||
|
},
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: ExcludeSemantics(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -159,14 +160,6 @@ class VoiceStatusChip extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: InkWell(
|
|
||||||
onTap: () {
|
|
||||||
HapticFeedback.lightImpact();
|
|
||||||
onTap();
|
|
||||||
},
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -190,63 +183,18 @@ class VoiceStatusChip extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 8),
|
||||||
),
|
Icon(
|
||||||
const SizedBox(width: 4),
|
|
||||||
IconButton(
|
|
||||||
tooltip: hardMuteByTalkPower
|
|
||||||
? l10n.voiceTalkPowerBlocked
|
|
||||||
: l10n.voiceHardMuteLabel,
|
|
||||||
icon: Icon(inputMuted ? Icons.mic_off : Icons.mic),
|
|
||||||
color: inputMuted ? theme.colorScheme.error : null,
|
|
||||||
onPressed: hardMuteByTalkPower ? null : onToggleInputMute,
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
constraints: const BoxConstraints.tightFor(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
style: const ButtonStyle(
|
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
tooltip: l10n.voiceOutputMuteLabel,
|
|
||||||
icon: Icon(outputMuted ? Icons.headset_off : Icons.headset),
|
|
||||||
color: outputMuted ? theme.colorScheme.error : null,
|
|
||||||
onPressed: onToggleOutputMute,
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
constraints: const BoxConstraints.tightFor(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
style: const ButtonStyle(
|
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
tooltip: l10n.voiceSettingsTitle,
|
|
||||||
icon: Icon(
|
|
||||||
Icons.expand_less,
|
Icons.expand_less,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
onPressed: onTap,
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
constraints: const BoxConstraints.tightFor(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
style: const ButtonStyle(
|
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -511,8 +459,6 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
|||||||
int _rateTickCount = 0;
|
int _rateTickCount = 0;
|
||||||
|
|
||||||
late final AudioProcessingConfigState _audioProcessing;
|
late final AudioProcessingConfigState _audioProcessing;
|
||||||
double? _streamLevel;
|
|
||||||
StreamSubscription<double>? _levelSub;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -521,12 +467,8 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
|||||||
widget.initialAudioConfig,
|
widget.initialAudioConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
_levelSub = rust.inputLevelStream().listen((level) {
|
// Poll audio stats at 250 ms so TX/RX counters and the level meter
|
||||||
if (mounted) setState(() => _streamLevel = level);
|
// update in real time while the sheet is open, independent of the parent.
|
||||||
});
|
|
||||||
|
|
||||||
// Poll audio stats at 250 ms so TX/RX counters update in real time
|
|
||||||
// while the sheet is open.
|
|
||||||
_statsTimer = Timer.periodic(const Duration(milliseconds: 250), (_) async {
|
_statsTimer = Timer.periodic(const Duration(milliseconds: 250), (_) async {
|
||||||
try {
|
try {
|
||||||
final s = await rust.audioStats();
|
final s = await rust.audioStats();
|
||||||
@@ -534,6 +476,7 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_stats = s;
|
_stats = s;
|
||||||
_rateTickCount++;
|
_rateTickCount++;
|
||||||
|
// Compute rates every ~1 s (4 × 250 ms).
|
||||||
if (_rateTickCount >= 4) {
|
if (_rateTickCount >= 4) {
|
||||||
_txRate = s.framesSent - _prevSent;
|
_txRate = s.framesSent - _prevSent;
|
||||||
_rxRate = s.framesReceived - _prevReceived;
|
_rxRate = s.framesReceived - _prevReceived;
|
||||||
@@ -548,7 +491,6 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_levelSub?.cancel();
|
|
||||||
_statsTimer?.cancel();
|
_statsTimer?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -601,6 +543,8 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
|||||||
Text(l10n.voiceSheetTitle, style: theme.textTheme.titleLarge),
|
Text(l10n.voiceSheetTitle, style: theme.textTheme.titleLarge),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// ── Primary section (always visible) ────────────────────
|
||||||
|
|
||||||
// 1) Audio output route picker tile (mobile only).
|
// 1) Audio output route picker tile (mobile only).
|
||||||
if (showRoutePicker) ...[
|
if (showRoutePicker) ...[
|
||||||
const AudioOutputTile(),
|
const AudioOutputTile(),
|
||||||
@@ -636,7 +580,7 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
|||||||
onTap: () => _setMode(rust.BridgeTransmitMode.voiceActivity),
|
onTap: () => _setMode(rust.BridgeTransmitMode.voiceActivity),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 3) Release-tail slider (PTT only).
|
// Release-tail slider (PTT only).
|
||||||
if (isPtt) ...[
|
if (isPtt) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
@@ -686,8 +630,8 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
|||||||
Divider(height: 1, color: theme.colorScheme.outlineVariant),
|
Divider(height: 1, color: theme.colorScheme.outlineVariant),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// 4) Level meter + live TX/RX stats.
|
// Level meter + live TX/RX stats.
|
||||||
VoiceLevelMeter(active: levelActive, level: _streamLevel ?? stats?.inputLevel),
|
VoiceLevelMeter(active: levelActive),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
_StatsRow(
|
_StatsRow(
|
||||||
txRate: _txRate,
|
txRate: _txRate,
|
||||||
@@ -710,18 +654,76 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
||||||
// Audio processing.
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Divider(height: 1, color: theme.colorScheme.outlineVariant),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
|
||||||
|
// ── Collapsible: Audio processing ───────────────────────
|
||||||
|
ExpansionTile(
|
||||||
|
initiallyExpanded: false,
|
||||||
|
shape: const Border(),
|
||||||
|
collapsedShape: const Border(),
|
||||||
|
tilePadding: const EdgeInsets.symmetric(horizontal: 0),
|
||||||
|
title: Text(
|
||||||
'Audio processing',
|
'Audio processing',
|
||||||
style: theme.textTheme.labelLarge?.copyWith(
|
style: theme.textTheme.labelLarge?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
children: [
|
||||||
|
_buildAudioProcessingSection(theme),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// ── Collapsible: PTT capability (PTT mode only) ─────────
|
||||||
|
if (isPtt)
|
||||||
|
ExpansionTile(
|
||||||
|
initiallyExpanded: false,
|
||||||
|
tilePadding: const EdgeInsets.symmetric(horizontal: 0),
|
||||||
|
shape: const Border(),
|
||||||
|
collapsedShape: const Border(),
|
||||||
|
title: Text(
|
||||||
|
'PTT capability',
|
||||||
|
style: theme.textTheme.labelLarge?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
PttCapabilityBadge(
|
||||||
|
level: widget.pttLevel,
|
||||||
|
backendId: widget.pttBackendId,
|
||||||
|
boundInputClass: widget.pttBoundInputClass,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// ── Collapsible: Debug ──────────────────────────────────
|
||||||
|
ExpansionTile(
|
||||||
|
initiallyExpanded: false,
|
||||||
|
shape: const Border(),
|
||||||
|
collapsedShape: const Border(),
|
||||||
|
tilePadding: const EdgeInsets.symmetric(horizontal: 0),
|
||||||
|
title: Text(
|
||||||
|
'Debug',
|
||||||
|
style: theme.textTheme.labelLarge?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
_buildDebugSection(theme),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Audio processing section (inside ExpansionTile) ─────────────────
|
||||||
|
|
||||||
|
Widget _buildAudioProcessingSection(ThemeData theme) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
// Android HW/SW selector.
|
// Android HW/SW selector.
|
||||||
if (Platform.isAndroid) ...[
|
if (Platform.isAndroid) ...[
|
||||||
const VoiceSubHeader('Processing backend'),
|
const VoiceSubHeader('Processing backend'),
|
||||||
@@ -848,16 +850,17 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
|||||||
_notifyAudioConfig();
|
_notifyAudioConfig();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
// Debug.
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
],
|
||||||
'Debug',
|
);
|
||||||
style: theme.textTheme.labelLarge?.copyWith(
|
}
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
// ── Debug section (inside ExpansionTile) ─────────────────────────────
|
||||||
),
|
|
||||||
const SizedBox(height: 2),
|
Widget _buildDebugSection(ThemeData theme) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
AudioProcessingToggleRow(
|
AudioProcessingToggleRow(
|
||||||
dense: true,
|
dense: true,
|
||||||
label: 'WAV dump',
|
label: 'WAV dump',
|
||||||
@@ -868,23 +871,8 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
|||||||
_notifyAudioConfig();
|
_notifyAudioConfig();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
// 6) PTT capability badge. On iOS this must remain
|
|
||||||
// visible even though the resolved level is always
|
|
||||||
// `L0Focused`, because the P0 acceptance flow requires
|
|
||||||
// honest capability advertising with an explanation of
|
|
||||||
// the sandbox limitation.
|
|
||||||
if (isPtt) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
PttCapabilityBadge(
|
|
||||||
level: widget.pttLevel,
|
|
||||||
backendId: widget.pttBackendId,
|
|
||||||
boundInputClass: widget.pttBoundInputClass,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1010,3 +998,424 @@ class _ModeRow extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Unified mobile voice bar ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// A unified bottom-anchored voice bar for compact / mobile layouts.
|
||||||
|
///
|
||||||
|
/// Combines the former [VoiceStatusChip] and [VoicePttButton] into one
|
||||||
|
/// visual zone with two rows:
|
||||||
|
///
|
||||||
|
/// ┌──────────────────────────────────────────────┐
|
||||||
|
/// │ 🟢 PTT · Connected [🔇] [🎧] [▲] │ ← control row (tap)
|
||||||
|
/// ├──────────────────────────────────────────────┤
|
||||||
|
/// │ ════ hold to talk ════ │ ← PTT row (hold)
|
||||||
|
/// └──────────────────────────────────────────────┘
|
||||||
|
///
|
||||||
|
/// The PTT row is shown only when [transmitMode] is PTT; for continuous
|
||||||
|
/// or voice-activity modes the bar shrinks to the control row alone.
|
||||||
|
///
|
||||||
|
/// State colour is applied to the entire container:
|
||||||
|
/// - normal: `surfaceContainerHigh`
|
||||||
|
/// - muted: `errorContainer` (35 % alpha)
|
||||||
|
/// - talk-power-block: amber (18 % alpha)
|
||||||
|
class CompactVoiceBar extends StatelessWidget {
|
||||||
|
/// Construct a compact voice bar.
|
||||||
|
const CompactVoiceBar({
|
||||||
|
super.key,
|
||||||
|
required this.inChannel,
|
||||||
|
required this.transmitMode,
|
||||||
|
required this.releaseTailMs,
|
||||||
|
required this.pttBoundKeyLabel,
|
||||||
|
required this.audioStats,
|
||||||
|
required this.isTouchOnly,
|
||||||
|
required this.inputMuted,
|
||||||
|
required this.outputMuted,
|
||||||
|
required this.onToggleInputMute,
|
||||||
|
required this.onToggleOutputMute,
|
||||||
|
required this.onOpenDetails,
|
||||||
|
required this.onPttHeldChanged,
|
||||||
|
this.talkPower,
|
||||||
|
this.neededTalkPower,
|
||||||
|
this.talkPowerGranted,
|
||||||
|
this.hardMuteByTalkPower = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// True when the client is inside a channel (gates PTT row visibility).
|
||||||
|
final bool inChannel;
|
||||||
|
|
||||||
|
/// Current transmit mode.
|
||||||
|
final rust.BridgeTransmitMode transmitMode;
|
||||||
|
|
||||||
|
/// Release-tail in milliseconds.
|
||||||
|
final int releaseTailMs;
|
||||||
|
|
||||||
|
/// Bound key label (empty on touch-only hosts).
|
||||||
|
final String pttBoundKeyLabel;
|
||||||
|
|
||||||
|
/// Current audio stats; null while audio engine not running.
|
||||||
|
final rust.BridgeAudioStats? audioStats;
|
||||||
|
|
||||||
|
/// True on iOS / iPadOS / Android.
|
||||||
|
final bool isTouchOnly;
|
||||||
|
|
||||||
|
/// True when local mic is muted.
|
||||||
|
final bool inputMuted;
|
||||||
|
|
||||||
|
/// True when local speaker is muted.
|
||||||
|
final bool outputMuted;
|
||||||
|
|
||||||
|
/// Toggle hard-mute on / off.
|
||||||
|
final VoidCallback onToggleInputMute;
|
||||||
|
|
||||||
|
/// Toggle output mute on / off.
|
||||||
|
final VoidCallback onToggleOutputMute;
|
||||||
|
|
||||||
|
/// Open the voice details modal sheet.
|
||||||
|
final VoidCallback onOpenDetails;
|
||||||
|
|
||||||
|
/// Called with `true` on finger-down, `false` on finger-up / cancel.
|
||||||
|
final ValueChanged<bool> onPttHeldChanged;
|
||||||
|
|
||||||
|
/// Own client's talk power.
|
||||||
|
final int? talkPower;
|
||||||
|
|
||||||
|
/// Talk power required to speak in current channel.
|
||||||
|
final int? neededTalkPower;
|
||||||
|
|
||||||
|
/// True when server granted talk power.
|
||||||
|
final bool? talkPowerGranted;
|
||||||
|
|
||||||
|
/// True when talk power prevents speaking.
|
||||||
|
final bool hardMuteByTalkPower;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final l10n = AppL10n.of(context);
|
||||||
|
final isPtt = transmitMode == rust.BridgeTransmitMode.ptt;
|
||||||
|
final pttActive = audioStats?.pttActive ?? false;
|
||||||
|
|
||||||
|
final summary = voiceStatusSummary(
|
||||||
|
l10n: l10n,
|
||||||
|
transmitMode: transmitMode,
|
||||||
|
releaseTailMs: releaseTailMs,
|
||||||
|
pttBoundKeyLabel: pttBoundKeyLabel,
|
||||||
|
isTouchOnly: isTouchOnly,
|
||||||
|
inputMuted: inputMuted,
|
||||||
|
outputMuted: outputMuted,
|
||||||
|
pttActive: pttActive,
|
||||||
|
talkPower: talkPower,
|
||||||
|
neededTalkPower: neededTalkPower,
|
||||||
|
talkPowerGranted: talkPowerGranted,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Container colour based on voice state.
|
||||||
|
final containerColor = summary.talkPowerBlocked
|
||||||
|
? Colors.amber.withValues(alpha: 0.18)
|
||||||
|
: summary.muted
|
||||||
|
? theme.colorScheme.errorContainer.withValues(alpha: 0.35)
|
||||||
|
: theme.colorScheme.surfaceContainerHigh;
|
||||||
|
|
||||||
|
final borderColor = summary.talkPowerBlocked
|
||||||
|
? Colors.amber.shade700
|
||||||
|
: summary.muted
|
||||||
|
? theme.colorScheme.error
|
||||||
|
: theme.colorScheme.outlineVariant;
|
||||||
|
|
||||||
|
final borderWidth = 1.0;
|
||||||
|
|
||||||
|
return Semantics(
|
||||||
|
label: '${l10n.voiceSheetTitle}: ${summary.line1}, ${summary.line2}',
|
||||||
|
child: Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: containerColor,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: borderColor, width: borderWidth),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: AnimatedSize(
|
||||||
|
duration: const Duration(milliseconds: 180),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// ── Control row ──────────────────────────────────────
|
||||||
|
_ControlRow(
|
||||||
|
summary: summary,
|
||||||
|
inputMuted: inputMuted,
|
||||||
|
outputMuted: outputMuted,
|
||||||
|
hardMuteByTalkPower: hardMuteByTalkPower,
|
||||||
|
onToggleInputMute: onToggleInputMute,
|
||||||
|
onToggleOutputMute: onToggleOutputMute,
|
||||||
|
onOpenDetails: onOpenDetails,
|
||||||
|
),
|
||||||
|
// ── PTT row (in-channel + PTT mode only) ──────────────
|
||||||
|
if (isPtt && inChannel) ...[
|
||||||
|
Divider(
|
||||||
|
height: 1,
|
||||||
|
thickness: 0.5,
|
||||||
|
color: borderColor,
|
||||||
|
indent: 12,
|
||||||
|
endIndent: 12,
|
||||||
|
),
|
||||||
|
_PttRow(
|
||||||
|
active: pttActive,
|
||||||
|
enabled: !hardMuteByTalkPower,
|
||||||
|
onHeldChanged: onPttHeldChanged,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Control row (tap-only zone) ───────────────────────────────────────────
|
||||||
|
|
||||||
|
class _ControlRow extends StatelessWidget {
|
||||||
|
const _ControlRow({
|
||||||
|
required this.summary,
|
||||||
|
required this.inputMuted,
|
||||||
|
required this.outputMuted,
|
||||||
|
required this.hardMuteByTalkPower,
|
||||||
|
required this.onToggleInputMute,
|
||||||
|
required this.onToggleOutputMute,
|
||||||
|
required this.onOpenDetails,
|
||||||
|
});
|
||||||
|
|
||||||
|
final VoiceStatusSummary summary;
|
||||||
|
final bool inputMuted;
|
||||||
|
final bool outputMuted;
|
||||||
|
final bool hardMuteByTalkPower;
|
||||||
|
final VoidCallback onToggleInputMute;
|
||||||
|
final VoidCallback onToggleOutputMute;
|
||||||
|
final VoidCallback onOpenDetails;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Status dot.
|
||||||
|
Icon(
|
||||||
|
summary.micOn ? Icons.fiber_manual_record : Icons.fiber_manual_record_outlined,
|
||||||
|
size: 10,
|
||||||
|
color: summary.micOn ? theme.colorScheme.primary : theme.colorScheme.outline,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
|
||||||
|
// Status text (tappable → open details).
|
||||||
|
Expanded(
|
||||||
|
child: Semantics(
|
||||||
|
button: true,
|
||||||
|
label: summary.line1,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onOpenDetails,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
summary.line1,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
summary.line2,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
|
||||||
|
// Mute button.
|
||||||
|
_ToggleButton(
|
||||||
|
icon: inputMuted ? Icons.mic_off : Icons.mic,
|
||||||
|
isActive: inputMuted,
|
||||||
|
tooltip: 'Mute mic',
|
||||||
|
onPressed: hardMuteByTalkPower ? null : onToggleInputMute,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Deafen button.
|
||||||
|
_ToggleButton(
|
||||||
|
icon: outputMuted ? Icons.headset_off : Icons.headset,
|
||||||
|
isActive: outputMuted,
|
||||||
|
tooltip: 'Deafen',
|
||||||
|
onPressed: onToggleOutputMute,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Settings / expand chevron.
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.expand_less, size: 20, color: theme.colorScheme.onSurfaceVariant),
|
||||||
|
tooltip: 'Voice settings',
|
||||||
|
onPressed: onOpenDetails,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Toggle button (mute / deafen) ─────────────────────────────────────────
|
||||||
|
|
||||||
|
class _ToggleButton extends StatelessWidget {
|
||||||
|
const _ToggleButton({
|
||||||
|
required this.icon,
|
||||||
|
required this.isActive,
|
||||||
|
required this.tooltip,
|
||||||
|
required this.onPressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final bool isActive;
|
||||||
|
final String tooltip;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return IconButton(
|
||||||
|
icon: Icon(icon, size: 22),
|
||||||
|
tooltip: tooltip,
|
||||||
|
color: isActive ? theme.colorScheme.error : null,
|
||||||
|
onPressed: onPressed,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PTT row (hold-only zone) ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
class _PttRow extends StatefulWidget {
|
||||||
|
const _PttRow({
|
||||||
|
required this.active,
|
||||||
|
required this.enabled,
|
||||||
|
required this.onHeldChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// True while the engine reports the gate open.
|
||||||
|
final bool active;
|
||||||
|
|
||||||
|
/// Whether the PTT button can be engaged.
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
|
/// Called with `true` on pointer-down, `false` on pointer-up / cancel.
|
||||||
|
final ValueChanged<bool> onHeldChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_PttRow> createState() => _PttRowState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PttRowState extends State<_PttRow> {
|
||||||
|
int? _activePointer;
|
||||||
|
bool _held = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
prepareVoiceHaptics();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _begin(PointerDownEvent event) {
|
||||||
|
if (!widget.enabled || _activePointer != null) return;
|
||||||
|
_activePointer = event.pointer;
|
||||||
|
_held = true;
|
||||||
|
widget.onHeldChanged(true);
|
||||||
|
playVoicePttHaptic(true);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _end(int pointer) {
|
||||||
|
if (_activePointer != pointer) return;
|
||||||
|
_activePointer = null;
|
||||||
|
_held = false;
|
||||||
|
widget.onHeldChanged(false);
|
||||||
|
playVoicePttHaptic(false);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final l10n = AppL10n.of(context);
|
||||||
|
final activeNow = _held || widget.active;
|
||||||
|
|
||||||
|
return Semantics(
|
||||||
|
button: true,
|
||||||
|
liveRegion: true,
|
||||||
|
label: activeNow ? l10n.pttTransmitting : l10n.pttHoldToTalk,
|
||||||
|
hint: l10n.pttHoldToTalkSemanticsHint,
|
||||||
|
child: Listener(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onPointerDown: _begin,
|
||||||
|
onPointerUp: (e) => _end(e.pointer),
|
||||||
|
onPointerCancel: (e) => _end(e.pointer),
|
||||||
|
child: ExcludeSemantics(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: activeNow
|
||||||
|
? theme.colorScheme.primary
|
||||||
|
: Colors.transparent,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
activeNow ? Icons.mic : Icons.mic_none_outlined,
|
||||||
|
size: 22,
|
||||||
|
color: activeNow
|
||||||
|
? theme.colorScheme.onPrimary
|
||||||
|
: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
activeNow ? l10n.voiceMicOn : l10n.voiceModePtt,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
color: activeNow
|
||||||
|
? theme.colorScheme.onPrimary
|
||||||
|
: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,78 +1,28 @@
|
|||||||
import 'dart:math' show max;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Shared compact level meter used by voice surfaces.
|
/// Shared compact level meter used by voice surfaces.
|
||||||
///
|
class VoiceLevelMeter extends StatelessWidget {
|
||||||
/// When [level] is null (no stats available yet), falls back to [active]
|
const VoiceLevelMeter({super.key, required this.active});
|
||||||
/// for a binary indicator. When [level] is provided it is interpreted as
|
|
||||||
/// dBFS and mapped to a 0–1 fill fraction via [dbfsToFraction] (floors
|
|
||||||
/// at -60 dBFS).
|
|
||||||
class VoiceLevelMeter extends StatefulWidget {
|
|
||||||
const VoiceLevelMeter({super.key, this.active = false, this.level});
|
|
||||||
|
|
||||||
/// Binary fallback when no dBFS value is available.
|
|
||||||
final bool active;
|
final bool active;
|
||||||
|
|
||||||
/// Real input level in dBFS (-120 = silence, 0 = clipping).
|
|
||||||
/// Null means stats are not yet available; [active] is used instead.
|
|
||||||
final double? level;
|
|
||||||
|
|
||||||
/// Map dBFS [-60, 0] → [0.0, 1.0].
|
|
||||||
static double dbfsToFraction(double dbfs) {
|
|
||||||
const floor = -60.0;
|
|
||||||
if (dbfs <= floor) return 0.0;
|
|
||||||
if (dbfs >= 0.0) return 1.0;
|
|
||||||
return (dbfs - floor) / -floor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<VoiceLevelMeter> createState() => _VoiceLevelMeterState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _VoiceLevelMeterState extends State<VoiceLevelMeter> {
|
|
||||||
double _previousFill = 0.0;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final double fill;
|
|
||||||
final Color color;
|
|
||||||
if (widget.level != null) {
|
|
||||||
fill = VoiceLevelMeter.dbfsToFraction(widget.level!);
|
|
||||||
color = fill > 0.0
|
|
||||||
? theme.colorScheme.primary
|
|
||||||
: theme.colorScheme.outlineVariant;
|
|
||||||
} else {
|
|
||||||
fill = widget.active ? 0.75 : 0.05;
|
|
||||||
color = widget.active
|
|
||||||
? theme.colorScheme.primary
|
|
||||||
: theme.colorScheme.outlineVariant;
|
|
||||||
}
|
|
||||||
|
|
||||||
final begin = _previousFill;
|
|
||||||
_previousFill = fill;
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: 8,
|
height: 8,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.surfaceContainerHighest,
|
color: theme.colorScheme.surfaceContainerHighest,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: TweenAnimationBuilder<double>(
|
child: FractionallySizedBox(
|
||||||
tween: Tween<double>(begin: begin, end: fill),
|
|
||||||
duration: const Duration(milliseconds: 120),
|
|
||||||
curve: Curves.easeOut,
|
|
||||||
builder: (context, animatedFill, child) {
|
|
||||||
return FractionallySizedBox(
|
|
||||||
alignment: AlignmentDirectional.centerStart,
|
alignment: AlignmentDirectional.centerStart,
|
||||||
widthFactor: max(animatedFill, 0.02),
|
widthFactor: active ? 0.75 : 0.05,
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color,
|
color: active
|
||||||
|
? theme.colorScheme.primary
|
||||||
|
: theme.colorScheme.outlineVariant,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Versions/Current/Resources
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleExecutable</key><string>chanora_bridge</string>
|
||||||
|
<key>CFBundleIdentifier</key><string>app.chanora.bridge</string>
|
||||||
|
<key>CFBundleName</key><string>chanora_bridge</string>
|
||||||
|
<key>CFBundlePackageType</key><string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key><string>1.0.0</string>
|
||||||
|
<key>CFBundleVersion</key><string>1</string>
|
||||||
|
<key>CFBundleSupportedPlatforms</key><array><string>MacOSX</string></array>
|
||||||
|
<key>MinimumOSVersion</key><string>10.15</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
|||||||
|
A
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Versions/Current/chanora_bridge
|
||||||
@@ -1,20 +1,57 @@
|
|||||||
PODS:
|
PODS:
|
||||||
|
- audio_session (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
- chanora_bridge (1.0.0)
|
- chanora_bridge (1.0.0)
|
||||||
|
- connectivity_plus (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
- FlutterMacOS (1.0.0)
|
- FlutterMacOS (1.0.0)
|
||||||
|
- package_info_plus (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- share_plus (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- shared_preferences_foundation (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
- url_launcher_macos (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
|
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
|
||||||
- chanora_bridge (from `/Users/edison/dev/chanora/apps/chanora_flutter/macos`)
|
- chanora_bridge (from `/Users/edison/dev/chanora/apps/chanora_flutter/macos`)
|
||||||
|
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
|
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||||
|
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||||
|
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
|
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
|
audio_session:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
|
||||||
chanora_bridge:
|
chanora_bridge:
|
||||||
:path: "/Users/edison/dev/chanora/apps/chanora_flutter/macos"
|
:path: "/Users/edison/dev/chanora/apps/chanora_flutter/macos"
|
||||||
|
connectivity_plus:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
|
||||||
FlutterMacOS:
|
FlutterMacOS:
|
||||||
:path: Flutter/ephemeral
|
:path: Flutter/ephemeral
|
||||||
|
package_info_plus:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
||||||
|
share_plus:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
|
||||||
|
shared_preferences_foundation:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||||
|
url_launcher_macos:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
|
audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e
|
||||||
chanora_bridge: 4105993843b5421ee4ce72220a74c63f6fd99103
|
chanora_bridge: 4105993843b5421ee4ce72220a74c63f6fd99103
|
||||||
|
connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e
|
||||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||||
|
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||||
|
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
|
||||||
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
|
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
||||||
|
|
||||||
PODFILE CHECKSUM: 99f0d126cab50f07c488b8550ebf033d2e8bcaeb
|
PODFILE CHECKSUM: 99f0d126cab50f07c488b8550ebf033d2e8bcaeb
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,6 @@
|
|||||||
45F255D1DE0134185DB5423D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 06E1AA7E1FB968C1D78DA8DE /* PrivacyInfo.xcprivacy */; };
|
45F255D1DE0134185DB5423D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 06E1AA7E1FB968C1D78DA8DE /* PrivacyInfo.xcprivacy */; };
|
||||||
9B86175918197ECC64969956 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EFEADEEFAB54DFEAAD7A70E9 /* Pods_Runner.framework */; };
|
9B86175918197ECC64969956 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EFEADEEFAB54DFEAAD7A70E9 /* Pods_Runner.framework */; };
|
||||||
C2DC22E19FDCE26B9D79442E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FDE04BBB936AA14C2B58FA0E /* Pods_RunnerTests.framework */; };
|
C2DC22E19FDCE26B9D79442E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FDE04BBB936AA14C2B58FA0E /* Pods_RunnerTests.framework */; };
|
||||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -94,7 +93,6 @@
|
|||||||
ED6F5EE0C5FD4DA22D79188C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
ED6F5EE0C5FD4DA22D79188C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
EFEADEEFAB54DFEAAD7A70E9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
EFEADEEFAB54DFEAAD7A70E9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
FDE04BBB936AA14C2B58FA0E /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
FDE04BBB936AA14C2B58FA0E /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -110,7 +108,6 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
|
||||||
8C6000042DD0000000000001 /* SileroCoreML in Frameworks */,
|
8C6000042DD0000000000001 /* SileroCoreML in Frameworks */,
|
||||||
9B86175918197ECC64969956 /* Pods_Runner.framework in Frameworks */,
|
9B86175918197ECC64969956 /* Pods_Runner.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
@@ -173,7 +170,6 @@
|
|||||||
33CEB47122A05771004F2AC0 /* Flutter */ = {
|
33CEB47122A05771004F2AC0 /* Flutter */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
|
||||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
|
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
|
||||||
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
|
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
|
||||||
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
|
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
|
||||||
@@ -260,7 +256,6 @@
|
|||||||
);
|
);
|
||||||
name = Runner;
|
name = Runner;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
|
||||||
8C6000032DD0000000000001 /* SileroCoreML */,
|
8C6000032DD0000000000001 /* SileroCoreML */,
|
||||||
);
|
);
|
||||||
productName = Runner;
|
productName = Runner;
|
||||||
@@ -307,7 +302,6 @@
|
|||||||
);
|
);
|
||||||
mainGroup = 33CC10E42044A3C60003C045;
|
mainGroup = 33CC10E42044A3C60003C045;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
|
||||||
8C6000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */,
|
8C6000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
|
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
|
||||||
@@ -871,10 +865,6 @@
|
|||||||
isa = XCLocalSwiftPackageReference;
|
isa = XCLocalSwiftPackageReference;
|
||||||
relativePath = ../../../silero-coreml;
|
relativePath = ../../../silero-coreml;
|
||||||
};
|
};
|
||||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
|
||||||
isa = XCLocalSwiftPackageReference;
|
|
||||||
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
|
||||||
};
|
|
||||||
/* End XCLocalSwiftPackageReference section */
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
@@ -883,10 +873,6 @@
|
|||||||
package = 8C6000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */;
|
package = 8C6000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */;
|
||||||
productName = SileroCoreML;
|
productName = SileroCoreML;
|
||||||
};
|
};
|
||||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
productName = FlutterGeneratedPluginSwiftPackage;
|
|
||||||
};
|
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
|
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
|
||||||
|
|||||||
@@ -5,24 +5,6 @@
|
|||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES">
|
buildImplicitDependencies = "YES">
|
||||||
<PreActions>
|
|
||||||
<ExecutionAction
|
|
||||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
|
||||||
<ActionContent
|
|
||||||
title = "Run Prepare Flutter Framework Script"
|
|
||||||
scriptText = ""$FLUTTER_ROOT"/packages/flutter_tools/bin/macos_assemble.sh prepare ">
|
|
||||||
<EnvironmentBuildable>
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
|
||||||
BuildableName = "chanora_flutter.app"
|
|
||||||
BlueprintName = "Runner"
|
|
||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</EnvironmentBuildable>
|
|
||||||
</ActionContent>
|
|
||||||
</ExecutionAction>
|
|
||||||
</PreActions>
|
|
||||||
<BuildActionEntries>
|
<BuildActionEntries>
|
||||||
<BuildActionEntry
|
<BuildActionEntry
|
||||||
buildForTesting = "YES"
|
buildForTesting = "YES"
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ class MacOSPermissionsHandler: NSObject, FlutterPlugin {
|
|||||||
// -- Input Monitoring ---------------------------------------------------
|
// -- Input Monitoring ---------------------------------------------------
|
||||||
case "checkInputMonitoring":
|
case "checkInputMonitoring":
|
||||||
result(inputMonitoringStateString())
|
result(inputMonitoringStateString())
|
||||||
startInputMonitoringPolling()
|
|
||||||
|
|
||||||
case "requestInputMonitoring":
|
case "requestInputMonitoring":
|
||||||
requestInputMonitoring(result: result)
|
requestInputMonitoring(result: result)
|
||||||
|
|||||||
@@ -457,10 +457,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.17.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -790,10 +790,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.11"
|
version: "0.7.10"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -493,7 +493,6 @@ void main() {
|
|||||||
framesSent: 1,
|
framesSent: 1,
|
||||||
framesReceived: 0,
|
framesReceived: 0,
|
||||||
pttActive: true,
|
pttActive: true,
|
||||||
inputLevel: -30.0,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -253,87 +253,49 @@ pub enum SessionEvent {
|
|||||||
},
|
},
|
||||||
/// Audio route changed (speaker/earpiece/BT/wired headset).
|
/// Audio route changed (speaker/earpiece/BT/wired headset).
|
||||||
AudioRouteChanged {
|
AudioRouteChanged {
|
||||||
/// New audio output route.
|
|
||||||
route: AudioRoute,
|
route: AudioRoute,
|
||||||
},
|
},
|
||||||
/// A client moved to a different channel.
|
|
||||||
ClientMoved {
|
ClientMoved {
|
||||||
/// Unique client identifier.
|
|
||||||
client_id: u64,
|
client_id: u64,
|
||||||
/// Destination channel.
|
|
||||||
new_channel_id: u64,
|
new_channel_id: u64,
|
||||||
},
|
},
|
||||||
/// A new client connected.
|
|
||||||
ClientJoined {
|
ClientJoined {
|
||||||
/// Unique client identifier.
|
|
||||||
client_id: u64,
|
client_id: u64,
|
||||||
/// Channel the client joined.
|
|
||||||
channel_id: u64,
|
channel_id: u64,
|
||||||
/// Display nickname.
|
|
||||||
name: String,
|
name: String,
|
||||||
/// Microphone muted state.
|
|
||||||
input_muted: bool,
|
input_muted: bool,
|
||||||
/// Speaker muted state.
|
|
||||||
output_muted: bool,
|
output_muted: bool,
|
||||||
/// Whether this is a server query (bot) client.
|
|
||||||
is_server_query: bool,
|
is_server_query: bool,
|
||||||
/// Client's talk power value.
|
|
||||||
talk_power: i32,
|
talk_power: i32,
|
||||||
/// Whether the server granted temporary talk power.
|
|
||||||
talk_power_granted: bool,
|
talk_power_granted: bool,
|
||||||
},
|
},
|
||||||
/// A client disconnected.
|
|
||||||
ClientLeft {
|
ClientLeft {
|
||||||
/// Unique client identifier.
|
|
||||||
client_id: u64,
|
client_id: u64,
|
||||||
/// Display nickname at time of disconnect.
|
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
/// Client properties changed.
|
|
||||||
ClientUpdated {
|
ClientUpdated {
|
||||||
/// Unique client identifier.
|
|
||||||
client_id: u64,
|
client_id: u64,
|
||||||
/// Microphone muted state.
|
|
||||||
input_muted: bool,
|
input_muted: bool,
|
||||||
/// Speaker muted state.
|
|
||||||
output_muted: bool,
|
output_muted: bool,
|
||||||
/// Whether this is a server query (bot) client.
|
|
||||||
is_server_query: bool,
|
is_server_query: bool,
|
||||||
/// Client's talk power value.
|
|
||||||
talk_power: i32,
|
talk_power: i32,
|
||||||
/// Whether the server granted temporary talk power.
|
|
||||||
talk_power_granted: bool,
|
talk_power_granted: bool,
|
||||||
},
|
},
|
||||||
/// A new channel appeared.
|
|
||||||
ChannelAdded {
|
ChannelAdded {
|
||||||
/// Unique channel identifier.
|
|
||||||
id: u64,
|
id: u64,
|
||||||
/// Parent channel ID.
|
|
||||||
parent: u64,
|
parent: u64,
|
||||||
/// Channel name.
|
|
||||||
name: String,
|
name: String,
|
||||||
/// Predecessor channel ID within the same parent (TeamSpeak
|
|
||||||
/// linked-list ordering hint). Zero means first child.
|
|
||||||
order: i64,
|
order: i64,
|
||||||
/// Whether the channel requires a password.
|
|
||||||
has_password: bool,
|
has_password: bool,
|
||||||
/// Talk power required to speak, or `None` when unrestricted.
|
|
||||||
needed_talk_power: Option<i32>,
|
needed_talk_power: Option<i32>,
|
||||||
},
|
},
|
||||||
/// A channel was deleted.
|
|
||||||
ChannelRemoved {
|
ChannelRemoved {
|
||||||
/// Channel identifier.
|
|
||||||
id: u64,
|
id: u64,
|
||||||
},
|
},
|
||||||
/// Channel properties changed.
|
|
||||||
ChannelUpdated {
|
ChannelUpdated {
|
||||||
/// Unique channel identifier.
|
|
||||||
id: u64,
|
id: u64,
|
||||||
/// Channel name.
|
|
||||||
name: String,
|
name: String,
|
||||||
/// Whether the channel requires a password.
|
|
||||||
has_password: bool,
|
has_password: bool,
|
||||||
/// Talk power required to speak, or `None` when unrestricted.
|
|
||||||
needed_talk_power: Option<i32>,
|
needed_talk_power: Option<i32>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1345,8 +1307,8 @@ impl ChanoraSession {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read audio engine statistics: (frames_sent, frames_received, transmit_active, input_level_dbfs).
|
/// Read audio engine statistics: (frames_sent, frames_received, transmit_active).
|
||||||
pub async fn audio_stats(&self) -> Result<(u32, u32, bool, f32), CoreError> {
|
pub async fn audio_stats(&self) -> Result<(u32, u32, bool), CoreError> {
|
||||||
let guard = self.inner.lock().await;
|
let guard = self.inner.lock().await;
|
||||||
let state = guard.as_ref().ok_or(CoreError::NotConnected)?;
|
let state = guard.as_ref().ok_or(CoreError::NotConnected)?;
|
||||||
let audio = state.audio.as_ref().ok_or(CoreError::AudioNotStarted)?;
|
let audio = state.audio.as_ref().ok_or(CoreError::AudioNotStarted)?;
|
||||||
@@ -1354,7 +1316,6 @@ impl ChanoraSession {
|
|||||||
audio.frames_sent(),
|
audio.frames_sent(),
|
||||||
audio.frames_received(),
|
audio.frames_received(),
|
||||||
audio.transmit_active(),
|
audio.transmit_active(),
|
||||||
audio.input_level(),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2438,7 +2399,6 @@ async fn supervisor_loop(ctx: SupervisorContext) {
|
|||||||
/// the UI would render. Two snapshots with identical channel
|
/// the UI would render. Two snapshots with identical channel
|
||||||
/// memberships, names, and orderings produce the same signature;
|
/// memberships, names, and orderings produce the same signature;
|
||||||
/// any in-channel move, rename, or reorder produces a different one.
|
/// any in-channel move, rename, or reorder produces a different one.
|
||||||
#[cfg(test)]
|
|
||||||
fn snapshot_signature(snap: &ServerSnapshot) -> u64 {
|
fn snapshot_signature(snap: &ServerSnapshot) -> u64 {
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2
|
|||||||
tokio = { version = "1", features = ["sync", "rt", "macros", "time"] }
|
tokio = { version = "1", features = ["sync", "rt", "macros", "time"] }
|
||||||
rustfft = "6.2.0"
|
rustfft = "6.2.0"
|
||||||
crossbeam = { version = "0.8", default-features = false, features = ["alloc", "crossbeam-queue"] }
|
crossbeam = { version = "0.8", default-features = false, features = ["alloc", "crossbeam-queue"] }
|
||||||
|
crossbeam-utils = { version = "0.8", default-features = false }
|
||||||
|
|
||||||
[target.'cfg(all(not(target_os = "android"), not(target_os = "ios"), not(target_os = "macos")))'.dependencies]
|
[target.'cfg(all(not(target_os = "android"), not(target_os = "ios"), not(target_os = "macos")))'.dependencies]
|
||||||
# Desktop audio I/O for Windows capture/playback and Linux capture.
|
# Desktop audio I/O for Windows capture/playback and Linux capture.
|
||||||
@@ -73,11 +74,6 @@ ndk-context = "0.1"
|
|||||||
# - deduplicated macro impls
|
# - deduplicated macro impls
|
||||||
# - PowerSavingOffloaded PerformanceMode variant
|
# - PowerSavingOffloaded PerformanceMode variant
|
||||||
oboe = { git = "https://github.com/EdisonJwa/oboe-rs", rev = "a14f9b83ecea8c93f5a692f2ee7808445b938c35" }
|
oboe = { git = "https://github.com/EdisonJwa/oboe-rs", rev = "a14f9b83ecea8c93f5a692f2ee7808445b938c35" }
|
||||||
# Safe slice reinterpret for the oboe stereo output callback.
|
|
||||||
# bytemuck::cast_slice_mut replaces the raw-pointer cast from
|
|
||||||
# `&mut [(f32, f32)]` to `&mut [f32]` with a provenance-correct
|
|
||||||
# and UB-free transmute backed by `NoUninit`.
|
|
||||||
bytemuck = { version = "1", features = ["derive"] }
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
# Real Windows global PTT (SDD-083 / SDD-084): RegisterRawInputDevices
|
# Real Windows global PTT (SDD-083 / SDD-084): RegisterRawInputDevices
|
||||||
|
|||||||
@@ -523,7 +523,7 @@ impl AudioInputCallback for InputCallback {
|
|||||||
|
|
||||||
struct OutputCallback {
|
struct OutputCallback {
|
||||||
handler: AudioHandler<SessionAudioId>,
|
handler: AudioHandler<SessionAudioId>,
|
||||||
event_consumer: crate::audio_event_queue::AudioEventConsumer,
|
event_queue: Arc<AudioEventQueue>,
|
||||||
output_gain: Arc<AtomicU32>,
|
output_gain: Arc<AtomicU32>,
|
||||||
output_muted: Arc<AtomicBool>,
|
output_muted: Arc<AtomicBool>,
|
||||||
event_tx: BackendEventTx,
|
event_tx: BackendEventTx,
|
||||||
@@ -542,12 +542,14 @@ impl AudioOutputCallback for OutputCallback {
|
|||||||
frames: &mut [(f32, f32)],
|
frames: &mut [(f32, f32)],
|
||||||
) -> DataCallbackResult {
|
) -> DataCallbackResult {
|
||||||
let _ = catch_unwind(AssertUnwindSafe(|| {
|
let _ = catch_unwind(AssertUnwindSafe(|| {
|
||||||
let buf: &mut [f32] =
|
let buf: &mut [f32] = unsafe {
|
||||||
bytemuck::cast_slice_mut::<(f32, f32), f32>(frames);
|
std::slice::from_raw_parts_mut(frames.as_mut_ptr() as *mut f32, frames.len() * 2)
|
||||||
|
};
|
||||||
for s in buf.iter_mut() {
|
for s in buf.iter_mut() {
|
||||||
*s = 0.0;
|
*s = 0.0;
|
||||||
}
|
}
|
||||||
for cmd in self.event_consumer.drain_controls() {
|
let consumer = AudioEventQueue::consumer(&self.event_queue);
|
||||||
|
for cmd in consumer.drain_controls() {
|
||||||
match cmd {
|
match cmd {
|
||||||
AudioCommand::SetVolume(id, vol) => {
|
AudioCommand::SetVolume(id, vol) => {
|
||||||
if let Some(q) = self.handler.get_mut_queues().get_mut(&id) {
|
if let Some(q) = self.handler.get_mut_queues().get_mut(&id) {
|
||||||
@@ -560,7 +562,7 @@ impl AudioOutputCallback for OutputCallback {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for pkt in self.event_consumer.drain_packets(50) {
|
for pkt in consumer.drain_packets(50) {
|
||||||
if let Err(e) = self.handler.handle_packet(pkt.client_id, pkt.data) {
|
if let Err(e) = self.handler.handle_packet(pkt.client_id, pkt.data) {
|
||||||
debug!(target: "chanora_audio", error = %e, "decode failed");
|
debug!(target: "chanora_audio", error = %e, "decode failed");
|
||||||
}
|
}
|
||||||
@@ -791,7 +793,7 @@ impl AndroidVoiceUnit {
|
|||||||
let event_queue = params.event_producer.queue();
|
let event_queue = params.event_producer.queue();
|
||||||
let output_cb = OutputCallback {
|
let output_cb = OutputCallback {
|
||||||
handler: params.handler,
|
handler: params.handler,
|
||||||
event_consumer: AudioEventQueue::consumer(&event_queue),
|
event_queue: event_queue.clone(),
|
||||||
output_gain: params.output_gain.clone(),
|
output_gain: params.output_gain.clone(),
|
||||||
output_muted: params.output_muted.clone(),
|
output_muted: params.output_muted.clone(),
|
||||||
event_tx: event_tx.clone(),
|
event_tx: event_tx.clone(),
|
||||||
@@ -814,7 +816,7 @@ impl AndroidVoiceUnit {
|
|||||||
cfg,
|
cfg,
|
||||||
&event_tx,
|
&event_tx,
|
||||||
AudioHandler::new(),
|
AudioHandler::new(),
|
||||||
AudioEventQueue::consumer(&event_queue),
|
event_queue.clone(),
|
||||||
params.output_gain.clone(),
|
params.output_gain.clone(),
|
||||||
params.output_muted.clone(),
|
params.output_muted.clone(),
|
||||||
audio_processing_stats.clone(),
|
audio_processing_stats.clone(),
|
||||||
@@ -1067,7 +1069,7 @@ impl AndroidVoiceUnit {
|
|||||||
cfg: &AndroidVoiceStreamConfig,
|
cfg: &AndroidVoiceStreamConfig,
|
||||||
event_tx: &BackendEventTx,
|
event_tx: &BackendEventTx,
|
||||||
handler: AudioHandler<SessionAudioId>,
|
handler: AudioHandler<SessionAudioId>,
|
||||||
event_consumer: crate::audio_event_queue::AudioEventConsumer,
|
event_queue: Arc<AudioEventQueue>,
|
||||||
output_gain: Arc<AtomicU32>,
|
output_gain: Arc<AtomicU32>,
|
||||||
output_muted: Arc<AtomicBool>,
|
output_muted: Arc<AtomicBool>,
|
||||||
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
||||||
@@ -1075,7 +1077,7 @@ impl AndroidVoiceUnit {
|
|||||||
) -> Result<AudioStreamAsync<OboeOutput, OutputCallback>, BackendError> {
|
) -> Result<AudioStreamAsync<OboeOutput, OutputCallback>, BackendError> {
|
||||||
let cb = OutputCallback {
|
let cb = OutputCallback {
|
||||||
handler,
|
handler,
|
||||||
event_consumer,
|
event_queue,
|
||||||
output_gain,
|
output_gain,
|
||||||
output_muted,
|
output_muted,
|
||||||
event_tx: event_tx.clone(),
|
event_tx: event_tx.clone(),
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ pub enum AudioCommand {
|
|||||||
/// Set a client's output volume.
|
/// Set a client's output volume.
|
||||||
SetVolume(SessionAudioId, f32),
|
SetVolume(SessionAudioId, f32),
|
||||||
/// Remove a client's decode queue.
|
/// Remove a client's decode queue.
|
||||||
// TODO: Wire to client disconnect path; handled in callback but no
|
|
||||||
// producer currently pushes this command.
|
|
||||||
RemoveClient(SessionAudioId),
|
RemoveClient(SessionAudioId),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -404,19 +404,6 @@ impl Default for SharedAudioProcessingStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SharedAudioProcessingStats {
|
impl SharedAudioProcessingStats {
|
||||||
/// Store the raw input dBFS level (desktop capture path).
|
|
||||||
/// Mobile platforms use [`Self::update_capture`] instead, which
|
|
||||||
/// also records VAD state; this lighter method is for the cpal
|
|
||||||
/// capture path that has no VAD pipeline.
|
|
||||||
pub fn set_input_dbfs(&self, dbfs: f32) {
|
|
||||||
self.input_dbfs.store(dbfs.to_bits(), Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read the current input dBFS level.
|
|
||||||
pub fn input_dbfs(&self) -> f32 {
|
|
||||||
f32::from_bits(self.input_dbfs.load(Ordering::Relaxed))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Store capture levels and VAD state.
|
/// Store capture levels and VAD state.
|
||||||
pub fn update_capture(
|
pub fn update_capture(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -838,7 +838,6 @@ impl AudioEngine {
|
|||||||
transmit_flag_for_capture,
|
transmit_flag_for_capture,
|
||||||
frames_sent.clone(),
|
frames_sent.clone(),
|
||||||
cfg.mic_gain,
|
cfg.mic_gain,
|
||||||
audio_processing_stats.clone(),
|
|
||||||
);
|
);
|
||||||
let (input_stream, capture_active) = match capture_result {
|
let (input_stream, capture_active) = match capture_result {
|
||||||
Ok(s) => (Some(s), true),
|
Ok(s) => (Some(s), true),
|
||||||
@@ -1598,11 +1597,6 @@ impl AudioEngine {
|
|||||||
self.frames_received.load(Ordering::Relaxed)
|
self.frames_received.load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Current microphone input level in dBFS (-120.0 = silence, 0.0 = clipping).
|
|
||||||
pub fn input_level(&self) -> f32 {
|
|
||||||
self.audio_processing_stats.input_dbfs()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Current audio-processing config snapshot.
|
/// Current audio-processing config snapshot.
|
||||||
pub fn audio_processing_config_snapshot(&self) -> crate::AudioProcessingConfig {
|
pub fn audio_processing_config_snapshot(&self) -> crate::AudioProcessingConfig {
|
||||||
self.audio_processing_config.lock().unwrap().clone()
|
self.audio_processing_config.lock().unwrap().clone()
|
||||||
@@ -1670,21 +1664,15 @@ impl AudioEngine {
|
|||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
{
|
{
|
||||||
let mut cmd = AudioCommand::SetVolume(SessionAudioId(client_id), clamped);
|
let mut cmd = AudioCommand::SetVolume(SessionAudioId(client_id), clamped);
|
||||||
for _ in 0..64 {
|
loop {
|
||||||
match self.audio_event_producer.push_control(cmd) {
|
match self.audio_event_producer.push_control(cmd) {
|
||||||
Ok(()) => return,
|
Ok(()) => break,
|
||||||
Err(returned) => {
|
Err(returned) => {
|
||||||
cmd = returned;
|
cmd = returned;
|
||||||
std::thread::yield_now();
|
std::thread::yield_now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tracing::warn!(
|
|
||||||
target: "chanora_audio",
|
|
||||||
client_id,
|
|
||||||
volume = clamped,
|
|
||||||
"set_client_volume: control queue full after 64 retries — volume not applied"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
{
|
{
|
||||||
@@ -1723,7 +1711,6 @@ fn try_open_capture(
|
|||||||
transmit_active: Arc<AtomicBool>,
|
transmit_active: Arc<AtomicBool>,
|
||||||
frames_sent: Arc<AtomicU32>,
|
frames_sent: Arc<AtomicU32>,
|
||||||
mic_gain: f32,
|
mic_gain: f32,
|
||||||
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
|
||||||
) -> Result<cpal::Stream, AudioError> {
|
) -> Result<cpal::Stream, AudioError> {
|
||||||
let in_cfg = in_dev
|
let in_cfg = in_dev
|
||||||
.default_input_config()
|
.default_input_config()
|
||||||
@@ -1759,7 +1746,6 @@ fn try_open_capture(
|
|||||||
voice_out_tx,
|
voice_out_tx,
|
||||||
transmit_active,
|
transmit_active,
|
||||||
frames_sent,
|
frames_sent,
|
||||||
audio_processing_stats,
|
|
||||||
)));
|
)));
|
||||||
|
|
||||||
let stream = match in_format {
|
let stream = match in_format {
|
||||||
@@ -1811,7 +1797,6 @@ struct CaptureState {
|
|||||||
/// capacity so the drain-into-frame path skips the allocator
|
/// capacity so the drain-into-frame path skips the allocator
|
||||||
/// after warmup. Same precedent as `mono_scratch` above.
|
/// after warmup. Same precedent as `mono_scratch` above.
|
||||||
frame_scratch: Vec<f32>,
|
frame_scratch: Vec<f32>,
|
||||||
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
|
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
|
||||||
@@ -1824,7 +1809,6 @@ impl CaptureState {
|
|||||||
voice_out_tx: mpsc::Sender<OutPacket>,
|
voice_out_tx: mpsc::Sender<OutPacket>,
|
||||||
transmit_active: Arc<AtomicBool>,
|
transmit_active: Arc<AtomicBool>,
|
||||||
frames_sent: Arc<AtomicU32>,
|
frames_sent: Arc<AtomicU32>,
|
||||||
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
encoder,
|
encoder,
|
||||||
@@ -1838,9 +1822,12 @@ impl CaptureState {
|
|||||||
voice_out_tx,
|
voice_out_tx,
|
||||||
transmit_active,
|
transmit_active,
|
||||||
frames_sent,
|
frames_sent,
|
||||||
|
// Generous upper bound for typical cpal periods
|
||||||
|
// (commonly 256..1024 frames); `clear()` retains the
|
||||||
|
// backing allocation across callbacks. See struct doc.
|
||||||
mono_scratch: Vec::with_capacity(4096),
|
mono_scratch: Vec::with_capacity(4096),
|
||||||
|
// Exact upper bound: drain pulls FRAME_SAMPLES at a time.
|
||||||
frame_scratch: Vec::with_capacity(FRAME_SAMPLES),
|
frame_scratch: Vec::with_capacity(FRAME_SAMPLES),
|
||||||
audio_processing_stats,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1848,8 +1835,17 @@ impl CaptureState {
|
|||||||
/// 48 kHz mono frames; encode and send when `transmit_active`
|
/// 48 kHz mono frames; encode and send when `transmit_active`
|
||||||
/// is true (PTT engaged).
|
/// is true (PTT engaged).
|
||||||
fn ingest<T: ToF32 + Copy>(&mut self, buf: &[T]) {
|
fn ingest<T: ToF32 + Copy>(&mut self, buf: &[T]) {
|
||||||
// 1. Down-mix to mono (pre-gain). Always performed so the level
|
if !self.transmit_active.load(Ordering::Relaxed) {
|
||||||
// meter reflects real mic input even when PTT is released.
|
// Drain accumulator while muted so we don't pop on PTT release.
|
||||||
|
self.pcm_accum.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Down-mix to mono + gain.
|
||||||
|
// Reuse `self.mono_scratch` to avoid a per-callback Vec
|
||||||
|
// allocation on the realtime audio thread; see struct
|
||||||
|
// doc and the engine.rs:1389-1397 precedent for why this
|
||||||
|
// matters for user-perceptible audio popping.
|
||||||
let in_channels = self.in_channels;
|
let in_channels = self.in_channels;
|
||||||
let mic_gain = self.mic_gain;
|
let mic_gain = self.mic_gain;
|
||||||
self.mono_scratch.clear();
|
self.mono_scratch.clear();
|
||||||
@@ -1857,23 +1853,8 @@ impl CaptureState {
|
|||||||
self.mono_scratch.reserve(frame_count);
|
self.mono_scratch.reserve(frame_count);
|
||||||
for frame in buf.chunks(in_channels) {
|
for frame in buf.chunks(in_channels) {
|
||||||
let sum: f32 = frame.iter().map(|s| s.to_f32_sample()).sum();
|
let sum: f32 = frame.iter().map(|s| s.to_f32_sample()).sum();
|
||||||
self.mono_scratch.push(sum / frame.len() as f32);
|
self.mono_scratch
|
||||||
}
|
.push((sum / frame.len() as f32) * mic_gain);
|
||||||
|
|
||||||
// Measure dBFS from pre-gain samples so the level meter
|
|
||||||
// reflects the raw mic input, not the amplified signal.
|
|
||||||
self.audio_processing_stats
|
|
||||||
.set_input_dbfs(crate::frame::dbfs(&self.mono_scratch));
|
|
||||||
|
|
||||||
if mic_gain != 1.0 {
|
|
||||||
for s in &mut self.mono_scratch {
|
|
||||||
*s *= mic_gain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.transmit_active.load(Ordering::Relaxed) {
|
|
||||||
self.pcm_accum.clear();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Resample to 48 kHz if needed. We re-borrow
|
// 2. Resample to 48 kHz if needed. We re-borrow
|
||||||
@@ -2501,7 +2482,6 @@ pub mod bench_seam {
|
|||||||
tx,
|
tx,
|
||||||
transmit_active.clone(),
|
transmit_active.clone(),
|
||||||
frames_sent,
|
frames_sent,
|
||||||
Arc::new(crate::SharedAudioProcessingStats::default()),
|
|
||||||
);
|
);
|
||||||
Self {
|
Self {
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ use std::sync::atomic::{AtomicBool, AtomicU32};
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
use tsclientlib::audio::AudioHandler;
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
use tsclientlib::audio::AudioHandler;
|
use tsclientlib::audio::AudioHandler;
|
||||||
|
|
||||||
use crate::engine::SessionAudioId;
|
use crate::engine::SessionAudioId;
|
||||||
|
|||||||
@@ -1038,8 +1038,6 @@ pub struct BridgeAudioStats {
|
|||||||
pub frames_received: u32,
|
pub frames_received: u32,
|
||||||
/// Current push-to-talk state.
|
/// Current push-to-talk state.
|
||||||
pub ptt_active: bool,
|
pub ptt_active: bool,
|
||||||
/// Current microphone input level in dBFS (-120.0 = silence, 0.0 = clipping).
|
|
||||||
pub input_level: f32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bridge route class for P1 audio-processing policy.
|
/// Bridge route class for P1 audio-processing policy.
|
||||||
@@ -1703,87 +1701,49 @@ pub enum BridgeEvent {
|
|||||||
},
|
},
|
||||||
/// Audio route changed (speaker/earpiece/BT/wired).
|
/// Audio route changed (speaker/earpiece/BT/wired).
|
||||||
AudioRouteChanged {
|
AudioRouteChanged {
|
||||||
/// New audio output route.
|
|
||||||
route: BridgeAudioRoute,
|
route: BridgeAudioRoute,
|
||||||
},
|
},
|
||||||
/// A client moved to a different channel.
|
|
||||||
ClientMoved {
|
ClientMoved {
|
||||||
/// Unique client identifier.
|
|
||||||
client_id: u64,
|
client_id: u64,
|
||||||
/// Destination channel.
|
|
||||||
new_channel_id: u64,
|
new_channel_id: u64,
|
||||||
},
|
},
|
||||||
/// A new client connected.
|
|
||||||
ClientJoined {
|
ClientJoined {
|
||||||
/// Unique client identifier.
|
|
||||||
client_id: u64,
|
client_id: u64,
|
||||||
/// Channel the client joined.
|
|
||||||
channel_id: u64,
|
channel_id: u64,
|
||||||
/// Display nickname.
|
|
||||||
name: String,
|
name: String,
|
||||||
/// Microphone muted state.
|
|
||||||
input_muted: bool,
|
input_muted: bool,
|
||||||
/// Speaker muted state.
|
|
||||||
output_muted: bool,
|
output_muted: bool,
|
||||||
/// True for server query (bot) clients.
|
|
||||||
is_server_query: bool,
|
is_server_query: bool,
|
||||||
/// Client's talk power value.
|
|
||||||
talk_power: i32,
|
talk_power: i32,
|
||||||
/// Whether the server granted temporary talk power.
|
|
||||||
talk_power_granted: bool,
|
talk_power_granted: bool,
|
||||||
},
|
},
|
||||||
/// A client disconnected.
|
|
||||||
ClientLeft {
|
ClientLeft {
|
||||||
/// Unique client identifier.
|
|
||||||
client_id: u64,
|
client_id: u64,
|
||||||
/// Display nickname at time of disconnect.
|
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
/// Client properties changed.
|
|
||||||
ClientUpdated {
|
ClientUpdated {
|
||||||
/// Unique client identifier.
|
|
||||||
client_id: u64,
|
client_id: u64,
|
||||||
/// Microphone muted state.
|
|
||||||
input_muted: bool,
|
input_muted: bool,
|
||||||
/// Speaker muted state.
|
|
||||||
output_muted: bool,
|
output_muted: bool,
|
||||||
/// True for server query (bot) clients.
|
|
||||||
is_server_query: bool,
|
is_server_query: bool,
|
||||||
/// Client's talk power value.
|
|
||||||
talk_power: i32,
|
talk_power: i32,
|
||||||
/// Whether the server granted temporary talk power.
|
|
||||||
talk_power_granted: bool,
|
talk_power_granted: bool,
|
||||||
},
|
},
|
||||||
/// A new channel appeared.
|
|
||||||
ChannelAdded {
|
ChannelAdded {
|
||||||
/// Unique channel identifier.
|
|
||||||
id: u64,
|
id: u64,
|
||||||
/// Parent channel ID.
|
|
||||||
parent: u64,
|
parent: u64,
|
||||||
/// Channel name.
|
|
||||||
name: String,
|
name: String,
|
||||||
/// Predecessor channel ID within the same parent (TeamSpeak
|
|
||||||
/// linked-list ordering hint). Zero means first child.
|
|
||||||
order: i64,
|
order: i64,
|
||||||
/// Whether the channel requires a password.
|
|
||||||
has_password: bool,
|
has_password: bool,
|
||||||
/// Talk power required to speak; `None` means no restriction.
|
|
||||||
needed_talk_power: Option<i32>,
|
needed_talk_power: Option<i32>,
|
||||||
},
|
},
|
||||||
/// A channel was deleted.
|
|
||||||
ChannelRemoved {
|
ChannelRemoved {
|
||||||
/// Channel identifier.
|
|
||||||
id: u64,
|
id: u64,
|
||||||
},
|
},
|
||||||
/// Channel properties changed.
|
|
||||||
ChannelUpdated {
|
ChannelUpdated {
|
||||||
/// Unique channel identifier.
|
|
||||||
id: u64,
|
id: u64,
|
||||||
/// Channel name.
|
|
||||||
name: String,
|
name: String,
|
||||||
/// Whether the channel requires a password.
|
|
||||||
has_password: bool,
|
has_password: bool,
|
||||||
/// Talk power required to speak; `None` means no restriction.
|
|
||||||
needed_talk_power: Option<i32>,
|
needed_talk_power: Option<i32>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -2098,7 +2058,7 @@ pub fn events_stream(sink: StreamSink<BridgeEvent>) -> Result<(), BridgeError> {
|
|||||||
|
|
||||||
/// Read audio statistics. Errors if no connection or audio not started.
|
/// Read audio statistics. Errors if no connection or audio not started.
|
||||||
pub async fn audio_stats() -> Result<BridgeAudioStats, BridgeError> {
|
pub async fn audio_stats() -> Result<BridgeAudioStats, BridgeError> {
|
||||||
let (s, r, p, lvl) = runtime()
|
let (s, r, p) = runtime()
|
||||||
.spawn(async { session().audio_stats().await })
|
.spawn(async { session().audio_stats().await })
|
||||||
.await
|
.await
|
||||||
.map_err(|e| task_join_error("audio_stats", e))??;
|
.map_err(|e| task_join_error("audio_stats", e))??;
|
||||||
@@ -2106,42 +2066,9 @@ pub async fn audio_stats() -> Result<BridgeAudioStats, BridgeError> {
|
|||||||
frames_sent: s,
|
frames_sent: s,
|
||||||
frames_received: r,
|
frames_received: r,
|
||||||
ptt_active: p,
|
ptt_active: p,
|
||||||
input_level: lvl,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Subscribe to real-time microphone input level at ~30 Hz.
|
|
||||||
/// Values are dBFS (-120 = silence, 0 = clipping). The stream ends
|
|
||||||
/// when the Dart subscriber cancels, the session is dropped, or
|
|
||||||
/// the session becomes persistently unavailable.
|
|
||||||
pub fn input_level_stream(sink: StreamSink<f32>) -> Result<(), BridgeError> {
|
|
||||||
runtime().spawn(async move {
|
|
||||||
let mut interval = tokio::time::interval(Duration::from_millis(33));
|
|
||||||
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
|
||||||
let mut consecutive_errors = 0u32;
|
|
||||||
loop {
|
|
||||||
interval.tick().await;
|
|
||||||
let level = match session().audio_stats().await {
|
|
||||||
Ok((_, _, _, lvl)) => {
|
|
||||||
consecutive_errors = 0;
|
|
||||||
lvl
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
consecutive_errors += 1;
|
|
||||||
if consecutive_errors >= 10 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
-120.0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if sink.add(level).is_err() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply the P1 audio-processing config.
|
/// Apply the P1 audio-processing config.
|
||||||
pub async fn set_audio_processing_config(
|
pub async fn set_audio_processing_config(
|
||||||
config: BridgeAudioProcessingConfig,
|
config: BridgeAudioProcessingConfig,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
|||||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||||
);
|
);
|
||||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
||||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -20394775;
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 281698435;
|
||||||
|
|
||||||
// Section: executor
|
// Section: executor
|
||||||
|
|
||||||
@@ -738,42 +738,6 @@ fn wire__crate__api__init_storage_impl(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fn wire__crate__api__input_level_stream_impl(
|
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
|
||||||
rust_vec_len_: i32,
|
|
||||||
data_len_: i32,
|
|
||||||
) {
|
|
||||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
|
|
||||||
flutter_rust_bridge::for_generated::TaskInfo {
|
|
||||||
debug_name: "input_level_stream",
|
|
||||||
port: Some(port_),
|
|
||||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
|
||||||
},
|
|
||||||
move || {
|
|
||||||
let message = unsafe {
|
|
||||||
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
|
||||||
ptr_,
|
|
||||||
rust_vec_len_,
|
|
||||||
data_len_,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let mut deserializer =
|
|
||||||
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
|
||||||
let api_sink =
|
|
||||||
<StreamSink<f32, flutter_rust_bridge::for_generated::SseCodec>>::sse_decode(
|
|
||||||
&mut deserializer,
|
|
||||||
);
|
|
||||||
deserializer.end();
|
|
||||||
move |context| {
|
|
||||||
transform_result_sse::<_, crate::BridgeError>((move || {
|
|
||||||
let output_ok = crate::api::input_level_stream(api_sink)?;
|
|
||||||
Ok(output_ok)
|
|
||||||
})())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
fn wire__crate__api__is_connected_impl(
|
fn wire__crate__api__is_connected_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
@@ -1826,14 +1790,6 @@ impl SseDecode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SseDecode for StreamSink<f32, flutter_rust_bridge::for_generated::SseCodec> {
|
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
|
||||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
|
||||||
let mut inner = <String>::sse_decode(deserializer);
|
|
||||||
return StreamSink::deserialize(inner);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SseDecode for String {
|
impl SseDecode for String {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
@@ -2006,12 +1962,10 @@ impl SseDecode for crate::api::BridgeAudioStats {
|
|||||||
let mut var_framesSent = <u32>::sse_decode(deserializer);
|
let mut var_framesSent = <u32>::sse_decode(deserializer);
|
||||||
let mut var_framesReceived = <u32>::sse_decode(deserializer);
|
let mut var_framesReceived = <u32>::sse_decode(deserializer);
|
||||||
let mut var_pttActive = <bool>::sse_decode(deserializer);
|
let mut var_pttActive = <bool>::sse_decode(deserializer);
|
||||||
let mut var_inputLevel = <f32>::sse_decode(deserializer);
|
|
||||||
return crate::api::BridgeAudioStats {
|
return crate::api::BridgeAudioStats {
|
||||||
frames_sent: var_framesSent,
|
frames_sent: var_framesSent,
|
||||||
frames_received: var_framesReceived,
|
frames_received: var_framesReceived,
|
||||||
ptt_active: var_pttActive,
|
ptt_active: var_pttActive,
|
||||||
input_level: var_inputLevel,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2801,34 +2755,33 @@ fn pde_ffi_dispatcher_primary_impl(
|
|||||||
14 => wire__crate__api__get_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
|
14 => wire__crate__api__get_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
|
||||||
15 => wire__crate__api__get_transmit_mode_impl(port, ptr, rust_vec_len, data_len),
|
15 => wire__crate__api__get_transmit_mode_impl(port, ptr, rust_vec_len, data_len),
|
||||||
20 => wire__crate__api__init_storage_impl(port, ptr, rust_vec_len, data_len),
|
20 => wire__crate__api__init_storage_impl(port, ptr, rust_vec_len, data_len),
|
||||||
21 => wire__crate__api__input_level_stream_impl(port, ptr, rust_vec_len, data_len),
|
21 => wire__crate__api__is_connected_impl(port, ptr, rust_vec_len, data_len),
|
||||||
22 => wire__crate__api__is_connected_impl(port, ptr, rust_vec_len, data_len),
|
22 => wire__crate__api__list_audio_devices_impl(port, ptr, rust_vec_len, data_len),
|
||||||
23 => wire__crate__api__list_audio_devices_impl(port, ptr, rust_vec_len, data_len),
|
23 => wire__crate__api__list_bookmarks_impl(port, ptr, rust_vec_len, data_len),
|
||||||
24 => wire__crate__api__list_bookmarks_impl(port, ptr, rust_vec_len, data_len),
|
25 => wire__crate__api__move_to_channel_impl(port, ptr, rust_vec_len, data_len),
|
||||||
26 => wire__crate__api__move_to_channel_impl(port, ptr, rust_vec_len, data_len),
|
26 => wire__crate__api__prefetch_server_impl(port, ptr, rust_vec_len, data_len),
|
||||||
27 => wire__crate__api__prefetch_server_impl(port, ptr, rust_vec_len, data_len),
|
27 => wire__crate__api__ptt_descriptor_impl(port, ptr, rust_vec_len, data_len),
|
||||||
28 => wire__crate__api__ptt_descriptor_impl(port, ptr, rust_vec_len, data_len),
|
29 => wire__crate__api__send_chat_message_impl(port, ptr, rust_vec_len, data_len),
|
||||||
30 => wire__crate__api__send_chat_message_impl(port, ptr, rust_vec_len, data_len),
|
31 => wire__crate__api__set_audio_processing_config_impl(port, ptr, rust_vec_len, data_len),
|
||||||
32 => wire__crate__api__set_audio_processing_config_impl(port, ptr, rust_vec_len, data_len),
|
32 => wire__crate__api__set_client_volume_impl(port, ptr, rust_vec_len, data_len),
|
||||||
33 => wire__crate__api__set_client_volume_impl(port, ptr, rust_vec_len, data_len),
|
33 => wire__crate__api__set_hard_mute_impl(port, ptr, rust_vec_len, data_len),
|
||||||
34 => wire__crate__api__set_hard_mute_impl(port, ptr, rust_vec_len, data_len),
|
34 => wire__crate__api__set_input_device_impl(port, ptr, rust_vec_len, data_len),
|
||||||
35 => wire__crate__api__set_input_device_impl(port, ptr, rust_vec_len, data_len),
|
35 => wire__crate__api__set_input_muted_impl(port, ptr, rust_vec_len, data_len),
|
||||||
36 => wire__crate__api__set_input_muted_impl(port, ptr, rust_vec_len, data_len),
|
36 => {
|
||||||
37 => {
|
|
||||||
wire__crate__api__set_ios_voice_processing_mode_impl(port, ptr, rust_vec_len, data_len)
|
wire__crate__api__set_ios_voice_processing_mode_impl(port, ptr, rust_vec_len, data_len)
|
||||||
}
|
}
|
||||||
39 => wire__crate__api__set_output_device_impl(port, ptr, rust_vec_len, data_len),
|
38 => wire__crate__api__set_output_device_impl(port, ptr, rust_vec_len, data_len),
|
||||||
40 => wire__crate__api__set_output_gain_impl(port, ptr, rust_vec_len, data_len),
|
39 => wire__crate__api__set_output_gain_impl(port, ptr, rust_vec_len, data_len),
|
||||||
41 => wire__crate__api__set_output_muted_impl(port, ptr, rust_vec_len, data_len),
|
40 => wire__crate__api__set_output_muted_impl(port, ptr, rust_vec_len, data_len),
|
||||||
42 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len),
|
41 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len),
|
||||||
43 => wire__crate__api__set_ptt_binding_impl(port, ptr, rust_vec_len, data_len),
|
42 => wire__crate__api__set_ptt_binding_impl(port, ptr, rust_vec_len, data_len),
|
||||||
44 => wire__crate__api__set_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
|
43 => wire__crate__api__set_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
|
||||||
45 => wire__crate__api__set_transmit_mode_impl(port, ptr, rust_vec_len, data_len),
|
44 => wire__crate__api__set_transmit_mode_impl(port, ptr, rust_vec_len, data_len),
|
||||||
46 => wire__crate__api__set_vad_model_path_impl(port, ptr, rust_vec_len, data_len),
|
45 => wire__crate__api__set_vad_model_path_impl(port, ptr, rust_vec_len, data_len),
|
||||||
47 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len),
|
46 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len),
|
||||||
48 => wire__crate__api__update_bookmark_impl(port, ptr, rust_vec_len, data_len),
|
47 => wire__crate__api__update_bookmark_impl(port, ptr, rust_vec_len, data_len),
|
||||||
49 => wire__crate__api__voice_join_impl(port, ptr, rust_vec_len, data_len),
|
48 => wire__crate__api__voice_join_impl(port, ptr, rust_vec_len, data_len),
|
||||||
50 => wire__crate__api__voice_leave_impl(port, ptr, rust_vec_len, data_len),
|
49 => wire__crate__api__voice_leave_impl(port, ptr, rust_vec_len, data_len),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2850,10 +2803,10 @@ fn pde_ffi_dispatcher_sync_impl(
|
|||||||
data_len,
|
data_len,
|
||||||
),
|
),
|
||||||
19 => wire__crate__api__handle_route_change_impl(ptr, rust_vec_len, data_len),
|
19 => wire__crate__api__handle_route_change_impl(ptr, rust_vec_len, data_len),
|
||||||
25 => wire__crate__api__log_file_path_str_impl(ptr, rust_vec_len, data_len),
|
24 => wire__crate__api__log_file_path_str_impl(ptr, rust_vec_len, data_len),
|
||||||
29 => wire__crate__api__record_lifecycle_event_impl(ptr, rust_vec_len, data_len),
|
28 => wire__crate__api__record_lifecycle_event_impl(ptr, rust_vec_len, data_len),
|
||||||
31 => wire__crate__api__set_audio_output_route_impl(ptr, rust_vec_len, data_len),
|
30 => wire__crate__api__set_audio_output_route_impl(ptr, rust_vec_len, data_len),
|
||||||
38 => wire__crate__api__set_network_state_impl(ptr, rust_vec_len, data_len),
|
37 => wire__crate__api__set_network_state_impl(ptr, rust_vec_len, data_len),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3031,7 +2984,6 @@ impl flutter_rust_bridge::IntoDart for crate::api::BridgeAudioStats {
|
|||||||
self.frames_sent.into_into_dart().into_dart(),
|
self.frames_sent.into_into_dart().into_dart(),
|
||||||
self.frames_received.into_into_dart().into_dart(),
|
self.frames_received.into_into_dart().into_dart(),
|
||||||
self.ptt_active.into_into_dart().into_dart(),
|
self.ptt_active.into_into_dart().into_dart(),
|
||||||
self.input_level.into_into_dart().into_dart(),
|
|
||||||
]
|
]
|
||||||
.into_dart()
|
.into_dart()
|
||||||
}
|
}
|
||||||
@@ -3700,13 +3652,6 @@ impl SseEncode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SseEncode for StreamSink<f32, flutter_rust_bridge::for_generated::SseCodec> {
|
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
|
||||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
|
||||||
unimplemented!("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SseEncode for String {
|
impl SseEncode for String {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
@@ -3836,7 +3781,6 @@ impl SseEncode for crate::api::BridgeAudioStats {
|
|||||||
<u32>::sse_encode(self.frames_sent, serializer);
|
<u32>::sse_encode(self.frames_sent, serializer);
|
||||||
<u32>::sse_encode(self.frames_received, serializer);
|
<u32>::sse_encode(self.frames_received, serializer);
|
||||||
<bool>::sse_encode(self.ptt_active, serializer);
|
<bool>::sse_encode(self.ptt_active, serializer);
|
||||||
<f32>::sse_encode(self.input_level, serializer);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,13 +56,6 @@ type PendingMoves = HashMap<
|
|||||||
),
|
),
|
||||||
>;
|
>;
|
||||||
|
|
||||||
struct EventChannels {
|
|
||||||
voice_in: mpsc::Sender<InboundVoice>,
|
|
||||||
chat: mpsc::Sender<ChatMessage>,
|
|
||||||
activity: mpsc::Sender<ServerActivity>,
|
|
||||||
delta: mpsc::Sender<ProtocolDelta>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
enum SendTimeoutError<T> {
|
enum SendTimeoutError<T> {
|
||||||
Timeout(T),
|
Timeout(T),
|
||||||
@@ -285,12 +278,10 @@ impl ProtocolClient {
|
|||||||
cfg.clone(),
|
cfg.clone(),
|
||||||
rx,
|
rx,
|
||||||
voice_out_rx,
|
voice_out_rx,
|
||||||
EventChannels {
|
voice_in_tx,
|
||||||
voice_in: voice_in_tx,
|
chat_tx,
|
||||||
chat: chat_tx,
|
activity_tx,
|
||||||
activity: activity_tx,
|
delta_tx,
|
||||||
delta: delta_tx,
|
|
||||||
},
|
|
||||||
ready_tx,
|
ready_tx,
|
||||||
lost_tx,
|
lost_tx,
|
||||||
));
|
));
|
||||||
@@ -480,9 +471,6 @@ impl ProtocolClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Takes ownership of the delta receiver channel. Returns `None` if already
|
|
||||||
/// taken. Must be called exactly once during initialization to subscribe to
|
|
||||||
/// incremental state changes.
|
|
||||||
pub fn take_delta_rx(&self) -> Option<mpsc::Receiver<ProtocolDelta>> {
|
pub fn take_delta_rx(&self) -> Option<mpsc::Receiver<ProtocolDelta>> {
|
||||||
self.delta_rx.lock().ok().and_then(|mut g| g.take())
|
self.delta_rx.lock().ok().and_then(|mut g| g.take())
|
||||||
}
|
}
|
||||||
@@ -511,7 +499,10 @@ async fn connection_task(
|
|||||||
cfg: ConnectConfig,
|
cfg: ConnectConfig,
|
||||||
mut rx: mpsc::Receiver<Request>,
|
mut rx: mpsc::Receiver<Request>,
|
||||||
mut voice_out_rx: mpsc::Receiver<OutPacket>,
|
mut voice_out_rx: mpsc::Receiver<OutPacket>,
|
||||||
channels: EventChannels,
|
voice_in_tx: mpsc::Sender<InboundVoice>,
|
||||||
|
chat_tx: mpsc::Sender<ChatMessage>,
|
||||||
|
activity_tx: mpsc::Sender<ServerActivity>,
|
||||||
|
delta_tx: mpsc::Sender<ProtocolDelta>,
|
||||||
ready_tx: oneshot::Sender<Result<(), ProtocolError>>,
|
ready_tx: oneshot::Sender<Result<(), ProtocolError>>,
|
||||||
lost_tx: oneshot::Sender<DisconnectReason>,
|
lost_tx: oneshot::Sender<DisconnectReason>,
|
||||||
) {
|
) {
|
||||||
@@ -692,14 +683,14 @@ async fn connection_task(
|
|||||||
Ok(Some(Ok(item))) => {
|
Ok(Some(Ok(item))) => {
|
||||||
match item {
|
match item {
|
||||||
StreamItem::Audio(buf) => {
|
StreamItem::Audio(buf) => {
|
||||||
handle_audio_stream_item(&channels.voice_in, &mut voice_activity, buf).await;
|
handle_audio_stream_item(&voice_in_tx, &mut voice_activity, buf).await;
|
||||||
}
|
}
|
||||||
other => handle_non_audio_stream_item(
|
other => handle_non_audio_stream_item(
|
||||||
&con,
|
&con,
|
||||||
other,
|
other,
|
||||||
&channels.chat,
|
&chat_tx,
|
||||||
&channels.activity,
|
&activity_tx,
|
||||||
&channels.delta,
|
&delta_tx,
|
||||||
&mut pending_moves,
|
&mut pending_moves,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -736,11 +727,13 @@ async fn connection_task(
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
for handle in expired {
|
for handle in expired {
|
||||||
if let Some((_target_channel, Some(reply), _)) = pending_moves.remove(&handle) {
|
if let Some((_target_channel, reply, _)) = pending_moves.remove(&handle) {
|
||||||
|
if let Some(reply) = reply {
|
||||||
let _ = reply.send(Ok(()));
|
let _ = reply.send(Ok(()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Service at most one control request (non-blocking).
|
// 3. Service at most one control request (non-blocking).
|
||||||
match rx.try_recv() {
|
match rx.try_recv() {
|
||||||
@@ -798,7 +791,10 @@ async fn connection_task(
|
|||||||
let r = fetch_client_profile(
|
let r = fetch_client_profile(
|
||||||
&mut con,
|
&mut con,
|
||||||
client_id,
|
client_id,
|
||||||
&channels,
|
&voice_in_tx,
|
||||||
|
&chat_tx,
|
||||||
|
&activity_tx,
|
||||||
|
&delta_tx,
|
||||||
&mut pending_moves,
|
&mut pending_moves,
|
||||||
&mut voice_activity,
|
&mut voice_activity,
|
||||||
)
|
)
|
||||||
@@ -873,7 +869,7 @@ fn handle_non_audio_stream_item(
|
|||||||
} = &ev
|
} = &ev
|
||||||
{
|
{
|
||||||
let own_client = con.get_state().ok().map(|state| state.own_client);
|
let own_client = con.get_state().ok().map(|state| state.own_client);
|
||||||
if let Ok(state) = con.get_state() {
|
if let Some(state) = con.get_state().ok() {
|
||||||
if let Some(client) = state.clients.get(client_id) {
|
if let Some(client) = state.clients.get(client_id) {
|
||||||
let _ = delta_tx.try_send(ProtocolDelta::ClientMoved {
|
let _ = delta_tx.try_send(ProtocolDelta::ClientMoved {
|
||||||
client_id: client_id.0 as u64,
|
client_id: client_id.0 as u64,
|
||||||
@@ -1119,7 +1115,10 @@ fn send_text_to_mode(
|
|||||||
async fn fetch_client_profile(
|
async fn fetch_client_profile(
|
||||||
con: &mut Connection,
|
con: &mut Connection,
|
||||||
client_id: u64,
|
client_id: u64,
|
||||||
channels: &EventChannels,
|
voice_in_tx: &mpsc::Sender<InboundVoice>,
|
||||||
|
chat_tx: &mpsc::Sender<ChatMessage>,
|
||||||
|
activity_tx: &mpsc::Sender<ServerActivity>,
|
||||||
|
delta_tx: &mpsc::Sender<ProtocolDelta>,
|
||||||
pending_moves: &mut PendingMoves,
|
pending_moves: &mut PendingMoves,
|
||||||
voice_activity: &mut HashMap<u64, Instant>,
|
voice_activity: &mut HashMap<u64, Instant>,
|
||||||
) -> Result<ClientProfile, ProtocolError> {
|
) -> Result<ClientProfile, ProtocolError> {
|
||||||
@@ -1156,7 +1155,10 @@ async fn fetch_client_profile(
|
|||||||
let _ = request_messages(
|
let _ = request_messages(
|
||||||
con,
|
con,
|
||||||
build_command("servergrouplist", &[], &[]),
|
build_command("servergrouplist", &[], &[]),
|
||||||
channels,
|
voice_in_tx,
|
||||||
|
chat_tx,
|
||||||
|
activity_tx,
|
||||||
|
delta_tx,
|
||||||
pending_moves,
|
pending_moves,
|
||||||
voice_activity,
|
voice_activity,
|
||||||
)
|
)
|
||||||
@@ -1166,7 +1168,10 @@ async fn fetch_client_profile(
|
|||||||
let _ = request_messages(
|
let _ = request_messages(
|
||||||
con,
|
con,
|
||||||
build_command("channelgrouplist", &[], &[]),
|
build_command("channelgrouplist", &[], &[]),
|
||||||
channels,
|
voice_in_tx,
|
||||||
|
chat_tx,
|
||||||
|
activity_tx,
|
||||||
|
delta_tx,
|
||||||
pending_moves,
|
pending_moves,
|
||||||
voice_activity,
|
voice_activity,
|
||||||
)
|
)
|
||||||
@@ -1180,7 +1185,10 @@ async fn fetch_client_profile(
|
|||||||
&[("clid", client_id.to_string())],
|
&[("clid", client_id.to_string())],
|
||||||
&[],
|
&[],
|
||||||
),
|
),
|
||||||
channels,
|
voice_in_tx,
|
||||||
|
chat_tx,
|
||||||
|
activity_tx,
|
||||||
|
delta_tx,
|
||||||
pending_moves,
|
pending_moves,
|
||||||
voice_activity,
|
voice_activity,
|
||||||
)
|
)
|
||||||
@@ -1198,7 +1206,10 @@ async fn fetch_client_profile(
|
|||||||
if let Err(e) = request_messages(
|
if let Err(e) = request_messages(
|
||||||
con,
|
con,
|
||||||
build_command("getconnectioninfo", &[("clid", client_id.to_string())], &[]),
|
build_command("getconnectioninfo", &[("clid", client_id.to_string())], &[]),
|
||||||
channels,
|
voice_in_tx,
|
||||||
|
chat_tx,
|
||||||
|
activity_tx,
|
||||||
|
delta_tx,
|
||||||
pending_moves,
|
pending_moves,
|
||||||
voice_activity,
|
voice_activity,
|
||||||
)
|
)
|
||||||
@@ -1217,7 +1228,10 @@ async fn fetch_client_profile(
|
|||||||
request_client_db_info(
|
request_client_db_info(
|
||||||
con,
|
con,
|
||||||
database_id,
|
database_id,
|
||||||
channels,
|
voice_in_tx,
|
||||||
|
chat_tx,
|
||||||
|
activity_tx,
|
||||||
|
delta_tx,
|
||||||
pending_moves,
|
pending_moves,
|
||||||
voice_activity,
|
voice_activity,
|
||||||
)
|
)
|
||||||
@@ -1375,7 +1389,10 @@ fn client_profile_refresh_plan(
|
|||||||
async fn request_messages(
|
async fn request_messages(
|
||||||
con: &mut Connection,
|
con: &mut Connection,
|
||||||
command: OutCommand,
|
command: OutCommand,
|
||||||
channels: &EventChannels,
|
voice_in_tx: &mpsc::Sender<InboundVoice>,
|
||||||
|
chat_tx: &mpsc::Sender<ChatMessage>,
|
||||||
|
activity_tx: &mpsc::Sender<ServerActivity>,
|
||||||
|
delta_tx: &mpsc::Sender<ProtocolDelta>,
|
||||||
pending_moves: &mut PendingMoves,
|
pending_moves: &mut PendingMoves,
|
||||||
voice_activity: &mut HashMap<u64, Instant>,
|
voice_activity: &mut HashMap<u64, Instant>,
|
||||||
) -> Result<Vec<InMessage>, ProtocolError> {
|
) -> Result<Vec<InMessage>, ProtocolError> {
|
||||||
@@ -1411,14 +1428,14 @@ async fn request_messages(
|
|||||||
return Ok(messages);
|
return Ok(messages);
|
||||||
}
|
}
|
||||||
StreamItem::Audio(buf) => {
|
StreamItem::Audio(buf) => {
|
||||||
handle_audio_stream_item(&channels.voice_in, voice_activity, buf).await;
|
handle_audio_stream_item(voice_in_tx, voice_activity, buf).await;
|
||||||
}
|
}
|
||||||
other => handle_non_audio_stream_item(
|
other => handle_non_audio_stream_item(
|
||||||
con,
|
con,
|
||||||
other,
|
other,
|
||||||
&channels.chat,
|
chat_tx,
|
||||||
&channels.activity,
|
activity_tx,
|
||||||
&channels.delta,
|
delta_tx,
|
||||||
pending_moves,
|
pending_moves,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -1428,14 +1445,20 @@ async fn request_messages(
|
|||||||
async fn request_client_db_info(
|
async fn request_client_db_info(
|
||||||
con: &mut Connection,
|
con: &mut Connection,
|
||||||
dbid: tsclientlib::ClientDbId,
|
dbid: tsclientlib::ClientDbId,
|
||||||
channels: &EventChannels,
|
voice_in_tx: &mpsc::Sender<InboundVoice>,
|
||||||
|
chat_tx: &mpsc::Sender<ChatMessage>,
|
||||||
|
activity_tx: &mpsc::Sender<ServerActivity>,
|
||||||
|
delta_tx: &mpsc::Sender<ProtocolDelta>,
|
||||||
pending_moves: &mut PendingMoves,
|
pending_moves: &mut PendingMoves,
|
||||||
voice_activity: &mut HashMap<u64, Instant>,
|
voice_activity: &mut HashMap<u64, Instant>,
|
||||||
) -> Result<InClientDbInfoPart, ProtocolError> {
|
) -> Result<InClientDbInfoPart, ProtocolError> {
|
||||||
let messages = request_messages(
|
let messages = request_messages(
|
||||||
con,
|
con,
|
||||||
build_command("clientdbinfo", &[("cldbid", dbid.0.to_string())], &[]),
|
build_command("clientdbinfo", &[("cldbid", dbid.0.to_string())], &[]),
|
||||||
channels,
|
voice_in_tx,
|
||||||
|
chat_tx,
|
||||||
|
activity_tx,
|
||||||
|
delta_tx,
|
||||||
pending_moves,
|
pending_moves,
|
||||||
voice_activity,
|
voice_activity,
|
||||||
)
|
)
|
||||||
@@ -1758,7 +1781,9 @@ fn format_server_activity(con: &Connection, ev: &tsclientlib::events::Event) ->
|
|||||||
extra,
|
extra,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
extra.reason?;
|
if extra.reason.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let client = activity_client(con, *client_id)?;
|
let client = activity_client(con, *client_id)?;
|
||||||
let channel = activity_channel_name(con, client.channel)?;
|
let channel = activity_channel_name(con, client.channel)?;
|
||||||
Some(format!(
|
Some(format!(
|
||||||
@@ -2136,7 +2161,7 @@ fn forward_delta(
|
|||||||
id: PropertyId::Client(client_id),
|
id: PropertyId::Client(client_id),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
if let Ok(state) = con.get_state() {
|
if let Some(state) = con.get_state().ok() {
|
||||||
if let Some(client) = state.clients.get(client_id) {
|
if let Some(client) = state.clients.get(client_id) {
|
||||||
let _ = delta_tx.try_send(ProtocolDelta::ClientJoined {
|
let _ = delta_tx.try_send(ProtocolDelta::ClientJoined {
|
||||||
client_id: client_id.0 as u64,
|
client_id: client_id.0 as u64,
|
||||||
@@ -2153,14 +2178,16 @@ fn forward_delta(
|
|||||||
}
|
}
|
||||||
Event::PropertyRemoved {
|
Event::PropertyRemoved {
|
||||||
id: PropertyId::Client(_),
|
id: PropertyId::Client(_),
|
||||||
old: PropertyValue::Client(client),
|
old,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
|
if let PropertyValue::Client(client) = old {
|
||||||
let _ = delta_tx.try_send(ProtocolDelta::ClientLeft {
|
let _ = delta_tx.try_send(ProtocolDelta::ClientLeft {
|
||||||
client_id: client.id.0 as u64,
|
client_id: client.id.0 as u64,
|
||||||
name: client.name.clone(),
|
name: client.name.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Event::PropertyChanged {
|
Event::PropertyChanged {
|
||||||
id: PropertyId::ClientChannel(_),
|
id: PropertyId::ClientChannel(_),
|
||||||
..
|
..
|
||||||
@@ -2169,7 +2196,7 @@ fn forward_delta(
|
|||||||
id: PropertyId::Client(client_id),
|
id: PropertyId::Client(client_id),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
if let Ok(state) = con.get_state() {
|
if let Some(state) = con.get_state().ok() {
|
||||||
if let Some(client) = state.clients.get(client_id) {
|
if let Some(client) = state.clients.get(client_id) {
|
||||||
let _ = delta_tx.try_send(ProtocolDelta::ClientUpdated {
|
let _ = delta_tx.try_send(ProtocolDelta::ClientUpdated {
|
||||||
client_id: client_id.0 as u64,
|
client_id: client_id.0 as u64,
|
||||||
@@ -2186,7 +2213,7 @@ fn forward_delta(
|
|||||||
id: PropertyId::Channel(channel_id),
|
id: PropertyId::Channel(channel_id),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
if let Ok(state) = con.get_state() {
|
if let Some(state) = con.get_state().ok() {
|
||||||
if let Some(channel) = state.channels.get(channel_id) {
|
if let Some(channel) = state.channels.get(channel_id) {
|
||||||
let _ = delta_tx.try_send(ProtocolDelta::ChannelAdded {
|
let _ = delta_tx.try_send(ProtocolDelta::ChannelAdded {
|
||||||
id: channel_id.0,
|
id: channel_id.0,
|
||||||
@@ -2201,18 +2228,20 @@ fn forward_delta(
|
|||||||
}
|
}
|
||||||
Event::PropertyRemoved {
|
Event::PropertyRemoved {
|
||||||
id: PropertyId::Channel(_),
|
id: PropertyId::Channel(_),
|
||||||
old: PropertyValue::Channel(channel),
|
old,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
|
if let PropertyValue::Channel(channel) = old {
|
||||||
let _ = delta_tx.try_send(ProtocolDelta::ChannelRemoved {
|
let _ = delta_tx.try_send(ProtocolDelta::ChannelRemoved {
|
||||||
id: channel.id.0,
|
id: channel.id.0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Event::PropertyChanged {
|
Event::PropertyChanged {
|
||||||
id: PropertyId::Channel(channel_id),
|
id: PropertyId::Channel(channel_id),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
if let Ok(state) = con.get_state() {
|
if let Some(state) = con.get_state().ok() {
|
||||||
if let Some(channel) = state.channels.get(channel_id) {
|
if let Some(channel) = state.channels.get(channel_id) {
|
||||||
let _ = delta_tx.try_send(ProtocolDelta::ChannelUpdated {
|
let _ = delta_tx.try_send(ProtocolDelta::ChannelUpdated {
|
||||||
id: channel_id.0,
|
id: channel_id.0,
|
||||||
|
|||||||
@@ -168,93 +168,52 @@ pub struct ServerSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ChannelId {
|
impl ChannelId {
|
||||||
/// Root/top-level channel identifier.
|
|
||||||
pub const ROOT: ChannelId = ChannelId(0);
|
pub const ROOT: ChannelId = ChannelId(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Incremental state change emitted by the protocol adapter when the server tree changes.
|
|
||||||
///
|
|
||||||
/// Covers client joins, leaves, moves, and channel additions, removals, and updates.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum ProtocolDelta {
|
pub enum ProtocolDelta {
|
||||||
/// A client moved to a different channel.
|
|
||||||
ClientMoved {
|
ClientMoved {
|
||||||
/// Unique client identifier.
|
|
||||||
client_id: u64,
|
client_id: u64,
|
||||||
/// Destination channel identifier.
|
|
||||||
new_channel_id: u64,
|
new_channel_id: u64,
|
||||||
},
|
},
|
||||||
/// A new client appeared on the server.
|
|
||||||
ClientJoined {
|
ClientJoined {
|
||||||
/// Unique client identifier.
|
|
||||||
client_id: u64,
|
client_id: u64,
|
||||||
/// Channel the client joined.
|
|
||||||
channel_id: u64,
|
channel_id: u64,
|
||||||
/// Display nickname.
|
|
||||||
name: String,
|
name: String,
|
||||||
/// Microphone muted state.
|
|
||||||
input_muted: bool,
|
input_muted: bool,
|
||||||
/// Speaker muted state.
|
|
||||||
output_muted: bool,
|
output_muted: bool,
|
||||||
/// Whether the client is a server query client.
|
|
||||||
is_server_query: bool,
|
is_server_query: bool,
|
||||||
/// Client's talk power value.
|
|
||||||
talk_power: i32,
|
talk_power: i32,
|
||||||
/// Whether the server granted temporary talk power.
|
|
||||||
talk_power_granted: bool,
|
talk_power_granted: bool,
|
||||||
},
|
},
|
||||||
/// A client disconnected.
|
|
||||||
ClientLeft {
|
ClientLeft {
|
||||||
/// Unique client identifier.
|
|
||||||
client_id: u64,
|
client_id: u64,
|
||||||
/// Display nickname.
|
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
/// A client's properties changed.
|
|
||||||
ClientUpdated {
|
ClientUpdated {
|
||||||
/// Unique client identifier.
|
|
||||||
client_id: u64,
|
client_id: u64,
|
||||||
/// Microphone muted state.
|
|
||||||
input_muted: bool,
|
input_muted: bool,
|
||||||
/// Speaker muted state.
|
|
||||||
output_muted: bool,
|
output_muted: bool,
|
||||||
/// Whether the client is a server query client.
|
|
||||||
is_server_query: bool,
|
is_server_query: bool,
|
||||||
/// Client's talk power value.
|
|
||||||
talk_power: i32,
|
talk_power: i32,
|
||||||
/// Whether the server granted temporary talk power.
|
|
||||||
talk_power_granted: bool,
|
talk_power_granted: bool,
|
||||||
},
|
},
|
||||||
/// A new channel appeared.
|
|
||||||
ChannelAdded {
|
ChannelAdded {
|
||||||
/// Channel identifier.
|
|
||||||
id: u64,
|
id: u64,
|
||||||
/// Parent channel identifier.
|
|
||||||
parent: u64,
|
parent: u64,
|
||||||
/// Channel name.
|
|
||||||
name: String,
|
name: String,
|
||||||
/// Predecessor channel ID within the same parent (TeamSpeak
|
|
||||||
/// linked-list ordering hint). Zero means first child.
|
|
||||||
order: i64,
|
order: i64,
|
||||||
/// Whether the channel has a password.
|
|
||||||
has_password: bool,
|
has_password: bool,
|
||||||
/// Needed talk power, or `None` when unrestricted.
|
|
||||||
needed_talk_power: Option<i32>,
|
needed_talk_power: Option<i32>,
|
||||||
},
|
},
|
||||||
/// A channel was deleted.
|
|
||||||
ChannelRemoved {
|
ChannelRemoved {
|
||||||
/// Channel identifier.
|
|
||||||
id: u64,
|
id: u64,
|
||||||
},
|
},
|
||||||
/// Channel properties changed.
|
|
||||||
ChannelUpdated {
|
ChannelUpdated {
|
||||||
/// Channel identifier.
|
|
||||||
id: u64,
|
id: u64,
|
||||||
/// Channel name.
|
|
||||||
name: String,
|
name: String,
|
||||||
/// Whether the channel has a password.
|
|
||||||
has_password: bool,
|
has_password: bool,
|
||||||
/// Needed talk power, or `None` when unrestricted.
|
|
||||||
needed_talk_power: Option<i32>,
|
needed_talk_power: Option<i32>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -628,12 +628,6 @@ fn read_file_dek(path: &Path) -> Result<[u8; 32], StorageError> {
|
|||||||
/// headless / sandboxed environments force the file-fallback path
|
/// headless / sandboxed environments force the file-fallback path
|
||||||
/// without poking a real OS keyring (which would either prompt the
|
/// without poking a real OS keyring (which would either prompt the
|
||||||
/// user or block on a missing D-Bus session).
|
/// user or block on a missing D-Bus session).
|
||||||
#[cfg(any(
|
|
||||||
target_os = "linux",
|
|
||||||
target_os = "macos",
|
|
||||||
target_os = "windows",
|
|
||||||
target_os = "ios"
|
|
||||||
))]
|
|
||||||
fn keyring_disabled() -> bool {
|
fn keyring_disabled() -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
std::env::var("CHANORA_DISABLE_KEYRING").as_deref(),
|
std::env::var("CHANORA_DISABLE_KEYRING").as_deref(),
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ those terms.
|
|||||||
|
|
||||||
| License | Crate count |
|
| License | Crate count |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `Apache License 2.0` | 333 |
|
| `Apache License 2.0` | 331 |
|
||||||
| `MIT License` | 74 |
|
| `MIT License` | 74 |
|
||||||
| `Unicode License v3` | 19 |
|
| `Unicode License v3` | 19 |
|
||||||
| `BSD 3-Clause "New" or "Revised" License` | 14 |
|
| `BSD 3-Clause "New" or "Revised" License` | 14 |
|
||||||
@@ -117,7 +117,6 @@ those terms.
|
|||||||
| pin-utils | 0.1.0 | `Apache License 2.0` | <https://github.com/rust-lang-nursery/pin-utils> |
|
| pin-utils | 0.1.0 | `Apache License 2.0` | <https://github.com/rust-lang-nursery/pin-utils> |
|
||||||
| ecdsa | 0.16.9 | `Apache License 2.0` | <https://github.com/RustCrypto/signatures/tree/master/ecdsa> |
|
| ecdsa | 0.16.9 | `Apache License 2.0` | <https://github.com/RustCrypto/signatures/tree/master/ecdsa> |
|
||||||
| rfc6979 | 0.4.0 | `Apache License 2.0` | <https://github.com/RustCrypto/signatures/tree/master/rfc6979> |
|
| rfc6979 | 0.4.0 | `Apache License 2.0` | <https://github.com/RustCrypto/signatures/tree/master/rfc6979> |
|
||||||
| crossbeam | 0.8.4 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
|
|
||||||
| ppv-lite86 | 0.2.21 | `Apache License 2.0` | <https://github.com/cryptocorrosion/cryptocorrosion> |
|
| ppv-lite86 | 0.2.21 | `Apache License 2.0` | <https://github.com/cryptocorrosion/cryptocorrosion> |
|
||||||
| rustls-pki-types | 1.14.1 | `Apache License 2.0` | <https://github.com/rustls/pki-types> |
|
| rustls-pki-types | 1.14.1 | `Apache License 2.0` | <https://github.com/rustls/pki-types> |
|
||||||
| keyring | 3.6.3 | `Apache License 2.0` | <https://github.com/hwchen/keyring-rs.git> |
|
| keyring | 3.6.3 | `Apache License 2.0` | <https://github.com/hwchen/keyring-rs.git> |
|
||||||
@@ -145,7 +144,6 @@ those terms.
|
|||||||
| critical-section | 1.2.0 | `Apache License 2.0` | <https://github.com/rust-embedded/critical-section> |
|
| critical-section | 1.2.0 | `Apache License 2.0` | <https://github.com/rust-embedded/critical-section> |
|
||||||
| crossbeam-channel | 0.5.15 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
|
| crossbeam-channel | 0.5.15 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
|
||||||
| crossbeam-epoch | 0.9.18 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
|
| crossbeam-epoch | 0.9.18 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
|
||||||
| crossbeam-queue | 0.3.12 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
|
|
||||||
| crossbeam-utils | 0.8.21 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
|
| crossbeam-utils | 0.8.21 | `Apache License 2.0` | <https://github.com/crossbeam-rs/crossbeam> |
|
||||||
| dbus-secret-service | 4.1.0 | `Apache License 2.0` | <https://github.com/brotskydotcom/dbus-secret-service.git> |
|
| dbus-secret-service | 4.1.0 | `Apache License 2.0` | <https://github.com/brotskydotcom/dbus-secret-service.git> |
|
||||||
| displaydoc | 0.2.6 | `Apache License 2.0` | <https://github.com/yaahc/displaydoc> |
|
| displaydoc | 0.2.6 | `Apache License 2.0` | <https://github.com/yaahc/displaydoc> |
|
||||||
@@ -4995,213 +4993,6 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright 2019 The Crossbeam Project Developers
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Apache License 2.0
|
|
||||||
|
|
||||||
```
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
To apply the Apache License to your work, attach the following
|
||||||
|
|||||||
Reference in New Issue
Block a user