feat: integrate chat voice and diagnostics client

This commit is contained in:
Edison Jwa
2026-05-23 06:51:55 +09:00
parent 7e28791ec2
commit 7d5d8c2c90
93 changed files with 10273 additions and 3347 deletions
+220 -29
View File
@@ -10,7 +10,7 @@ import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
part 'api.freezed.dart';
// These functions are ignored because they are not marked as `pub`: `install_panic_diagnostic_hook`, `log_file_path`, `log_sink`, `map_join_error_code`, `map_join_sync_state`, `open_log_file`, `permission_events`, `publish_permission_state`, `runtime`, `session`, `task_join_error`, `transmit_mode_from_u8`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`
// These functions are ignored (category: IgnoreBecauseExplicitAttribute): `from_kotlin_str`, `to_permission_gate`
/// Return the platform-conventional log-file path as a string, or
@@ -46,15 +46,6 @@ Future<bool> isConnected() => RustLib.instance.api.crateApiIsConnected();
void handleRouteChange({required BridgeAudioRoute route}) =>
RustLib.instance.api.crateApiHandleRouteChange(route: route);
/// Handle iOS AVAudioSession media-services reset (legacy, no route arg).
///
/// Called by the existing FRB-generated Dart binding. Uses
/// `AudioRoute::Unknown` which triggers a route-change recompute.
/// The AppDelegate now also calls `handle_media_services_reset_with_route`
/// directly after rebuilding the session.
void handleMediaServicesReset() =>
RustLib.instance.api.crateApiHandleMediaServicesReset();
/// Handle iOS AVAudioSession media-services reset with the current
/// route class. Called by AppDelegate after rebuilding the session.
///
@@ -74,11 +65,11 @@ void handleInterruptionEnded({required bool shouldResume}) => RustLib
.api
.crateApiHandleInterruptionEnded(shouldResume: shouldResume);
/// Set the push-to-talk state.
/// Set the focused/on-screen push-to-talk hold state.
///
/// Superseded in v1 by [`set_transmit_mode`] + the binding capture
/// dialog. Retained so legacy callers and integration tests keep
/// working; the new VoiceBar UI no longer invokes this.
/// Binding capture chooses which physical key drives PTT, while this
/// command carries the actual press/release edge for fallback focused
/// keyboard handling and touch controls.
Future<void> setPtt({required bool active}) =>
RustLib.instance.api.crateApiSetPtt(active: active);
@@ -131,19 +122,17 @@ Future<void> setPttBinding({
platformKey: platformKey,
);
/// Read the current PTT capability descriptor. Returns a
/// `(level, backend_id, bound_input_class)` triple matching the
/// privacy-safe `BridgeEvent::PttCapability` event shape; useful
/// for the initial UI render before the first event arrives.
Future<(String, String, String)> pttDescriptor() =>
/// Read the current PTT capability descriptor. Matches the privacy-safe
/// `BridgeEvent::PttCapability` event shape; useful for the initial UI
/// render before the first event arrives.
Future<BridgePttDescriptor> pttDescriptor() =>
RustLib.instance.api.crateApiPttDescriptor();
/// Return the persisted PTT binding as a
/// `(input_class, platform_key)` pair so the UI can hydrate its
/// display state at launch (e.g. show "PTT: Space" next to the
/// badge before the user re-opens the binding dialog). Empty
/// strings mean no binding has been persisted yet.
Future<(String, String)> getPttBinding() =>
/// Return the persisted PTT binding so the UI can hydrate its display
/// state at launch (e.g. show "PTT: Space" next to the badge before the
/// user re-opens the binding dialog). Empty strings mean no binding has
/// been persisted yet.
Future<BridgePttBinding> getPttBinding() =>
RustLib.instance.api.crateApiGetPttBinding();
/// Move our own client to `channel_id`. Optional channel password
@@ -176,6 +165,27 @@ Future<void> setOutputMuted({required bool muted}) =>
Future<void> setOutputGain({required double gain}) =>
RustLib.instance.api.crateApiSetOutputGain(gain: gain);
/// Set per-client output volume (SRS-075). `1.0` is unity, `0.0`
/// mutes. No-op when client has no active voice queue. Volume is
/// applied directly to the tsclientlib AudioQueue and takes effect
/// immediately on the next render callback.
Future<void> setClientVolume({
required BigInt clientId,
required double volume,
}) => RustLib.instance.api.crateApiSetClientVolume(
clientId: clientId,
volume: volume,
);
/// Send a text message to the specified target.
Future<void> sendChatMessage({
required String message,
required BridgeMessageTarget target,
}) => RustLib.instance.api.crateApiSendChatMessage(
message: message,
target: target,
);
/// User-initiated diagnostic export. Returns a multi-line text
/// blob, redacted per the production policy, that the user can
/// share or copy. DEC-016 forbids automatic uploads — this is the
@@ -256,6 +266,19 @@ Future<BridgeAudioProcessingConfig> getAudioProcessingConfig() =>
Future<BridgeAudioProcessingStats> audioProcessingStats() =>
RustLib.instance.api.crateApiAudioProcessingStats();
/// List available audio input and output devices from the platform.
Future<BridgeAudioDeviceList> listAudioDevices() =>
RustLib.instance.api.crateApiListAudioDevices();
/// Set the preferred input device by name. Takes effect on next
/// `start_audio`.
Future<void> setInputDevice({String? name}) =>
RustLib.instance.api.crateApiSetInputDevice(name: name);
/// Set the preferred output device by name.
Future<void> setOutputDevice({String? name}) =>
RustLib.instance.api.crateApiSetOutputDevice(name: name);
/// Configure the VAD model path.
Future<void> setVadModelPath({required String path}) =>
RustLib.instance.api.crateApiSetVadModelPath(path: path);
@@ -273,6 +296,14 @@ Future<void> setIosVoiceProcessingMode({
required BridgeIosVoiceProcessingMode mode,
}) => RustLib.instance.api.crateApiSetIosVoiceProcessingMode(mode: mode);
/// Set the preferred audio output route (Android/iOS).
void setAudioOutputRoute({required BridgeAudioRoute route}) =>
RustLib.instance.api.crateApiSetAudioOutputRoute(route: route);
/// Called from Flutter when the app enters background/foreground.
void recordLifecycleEvent({required String state}) =>
RustLib.instance.api.crateApiRecordLifecycleEvent(state: state);
/// Bridge processing backend.
enum BridgeAudioBackend {
/// Platform voice processing.
@@ -288,6 +319,53 @@ enum BridgeAudioBackend {
noop,
}
/// Audio device info from the platform.
class BridgeAudioDevice {
/// Human-readable device name.
final String name;
/// True if the OS reports this as the default device.
final bool isDefault;
const BridgeAudioDevice({required this.name, required this.isDefault});
@override
int get hashCode => name.hashCode ^ isDefault.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeAudioDevice &&
runtimeType == other.runtimeType &&
name == other.name &&
isDefault == other.isDefault;
}
/// List of available audio devices.
class BridgeAudioDeviceList {
/// Available input devices.
final List<BridgeAudioDevice> inputDevices;
/// Available output devices.
final List<BridgeAudioDevice> outputDevices;
const BridgeAudioDeviceList({
required this.inputDevices,
required this.outputDevices,
});
@override
int get hashCode => inputDevices.hashCode ^ outputDevices.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeAudioDeviceList &&
runtimeType == other.runtimeType &&
inputDevices == other.inputDevices &&
outputDevices == other.outputDevices;
}
/// P1 audio-processing configuration DTO.
class BridgeAudioProcessingConfig {
/// Route class.
@@ -628,12 +706,17 @@ class BridgeChannel {
/// True when the server marks the channel as password-protected.
final bool hasPassword;
/// Talk power threshold required to speak in this channel.
/// None means no talk-power restriction.
final int? neededTalkPower;
const BridgeChannel({
required this.id,
required this.parent,
required this.name,
required this.order,
required this.hasPassword,
this.neededTalkPower,
});
@override
@@ -642,7 +725,8 @@ class BridgeChannel {
parent.hashCode ^
name.hashCode ^
order.hashCode ^
hasPassword.hashCode;
hasPassword.hashCode ^
neededTalkPower.hashCode;
@override
bool operator ==(Object other) =>
@@ -653,7 +737,8 @@ class BridgeChannel {
parent == other.parent &&
name == other.name &&
order == other.order &&
hasPassword == other.hasPassword;
hasPassword == other.hasPassword &&
neededTalkPower == other.neededTalkPower;
}
/// Client as seen by Dart.
@@ -679,6 +764,12 @@ class BridgeClient {
/// True for TeamSpeak ServerQuery clients.
final bool isServerQuery;
/// Current talk power value assigned by the server.
final int talkPower;
/// True when the server has granted talk power regardless of numeric value.
final bool talkPowerGranted;
const BridgeClient({
required this.id,
required this.channel,
@@ -687,6 +778,8 @@ class BridgeClient {
required this.outputMuted,
required this.isSpeaking,
required this.isServerQuery,
required this.talkPower,
required this.talkPowerGranted,
});
@override
@@ -697,7 +790,9 @@ class BridgeClient {
inputMuted.hashCode ^
outputMuted.hashCode ^
isSpeaking.hashCode ^
isServerQuery.hashCode;
isServerQuery.hashCode ^
talkPower.hashCode ^
talkPowerGranted.hashCode;
@override
bool operator ==(Object other) =>
@@ -710,7 +805,9 @@ class BridgeClient {
inputMuted == other.inputMuted &&
outputMuted == other.outputMuted &&
isSpeaking == other.isSpeaking &&
isServerQuery == other.isServerQuery;
isServerQuery == other.isServerQuery &&
talkPower == other.talkPower &&
talkPowerGranted == other.talkPowerGranted;
}
/// Bridge effect owner for AEC/NS/AGC.
@@ -863,6 +960,27 @@ sealed class BridgeEvent with _$BridgeEvent {
/// Resolved permission state.
required PermissionStateKind state,
}) = BridgeEvent_PermissionState;
/// A text message received from the server.
const factory BridgeEvent.chatMessage({
/// Client id of the sender.
required BigInt senderId,
/// Nickname of the sender.
required String senderName,
/// Message content.
required String message,
/// Target scope (server/channel/private/poke).
required BridgeMessageTarget target,
}) = BridgeEvent_ChatMessage;
/// Audio route changed (speaker/earpiece/BT/wired).
const factory BridgeEvent.audioRouteChanged({
/// The new audio route.
required BridgeAudioRoute route,
}) = BridgeEvent_AudioRouteChanged;
}
/// Bridge iOS voice-processing mode.
@@ -874,6 +992,25 @@ enum BridgeIosVoiceProcessingMode {
sonoraExperimental,
}
@freezed
sealed class BridgeMessageTarget with _$BridgeMessageTarget {
const BridgeMessageTarget._();
/// Broadcast to entire server.
const factory BridgeMessageTarget.server() = BridgeMessageTarget_Server;
/// Broadcast to current channel.
const factory BridgeMessageTarget.channel() = BridgeMessageTarget_Channel;
/// Private message to a specific client.
const factory BridgeMessageTarget.client(BigInt field0) =
BridgeMessageTarget_Client;
/// Poke a specific client.
const factory BridgeMessageTarget.poke(BigInt field0) =
BridgeMessageTarget_Poke;
}
/// Coarse OS-reported network state. Mirrors
/// [`chanora_core::NetworkState`] across the bridge.
enum BridgeNetworkState {
@@ -887,6 +1024,60 @@ enum BridgeNetworkState {
offline,
}
/// Persisted PTT binding display state for the UI.
class BridgePttBinding {
/// Stable input category string (`""`, `"keyboard"`, or
/// `"mouse-side-button"`).
final String inputClass;
/// Display-only key label; empty when no binding is active.
final String keyLabel;
const BridgePttBinding({required this.inputClass, required this.keyLabel});
@override
int get hashCode => inputClass.hashCode ^ keyLabel.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgePttBinding &&
runtimeType == other.runtimeType &&
inputClass == other.inputClass &&
keyLabel == other.keyLabel;
}
/// Privacy-safe PTT capability descriptor for the UI.
class BridgePttDescriptor {
/// Stable capability level name.
final String level;
/// Stable backend identifier.
final String backendId;
/// Coarse bound input class; empty when no binding is active.
final String boundInputClass;
const BridgePttDescriptor({
required this.level,
required this.backendId,
required this.boundInputClass,
});
@override
int get hashCode =>
level.hashCode ^ backendId.hashCode ^ boundInputClass.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgePttDescriptor &&
runtimeType == other.runtimeType &&
level == other.level &&
backendId == other.backendId &&
boundInputClass == other.boundInputClass;
}
/// Coarse PTT input class (gen2 v0.9.3 / DEC-026). Stable strings;
/// the bridge never carries raw key codes.
enum BridgePttInputClass {