feat(voice): real-time mic input level metering at 30 Hz (#25)
* feat(voice): add real-time mic input level metering at 30 Hz
Expose input RMS from the audio engine through the bridge as a
dedicated Rust→Dart Stream<double>, replacing the binary on/off
indicator with a proportional dBFS level meter.
Rust side:
- chanora_audio: add set_input_dbfs/input_dbfs accessors to
SharedAudioProcessingStats; restructure CaptureState::ingest()
to compute dBFS from mono buffer before the PTT guard so the
meter shows mic activity even when not transmitting.
- chanora_core: widen audio_stats() return to include f32 input
level.
- chanora_bridge: add input_level: f32 to BridgeAudioStats and
new input_level_stream(sink: StreamSink<f32>) that pushes at
~30 Hz via tokio interval task.
- Update frb_generated.rs serialization for the new field.
Flutter side:
- VoiceLevelMeter: accept optional double level (dBFS), map
-60..0 dBFS to 0..1 fill fraction, animate with
TweenAnimationBuilder for smooth transitions.
- voice_compact.dart: subscribe to inputLevelStream in the voice
details sheet for 30 Hz meter updates, keeping 250 ms poll for
TX/RX counters.
- voice_bar.dart: accept optional inputLevel from the stream.
- main.dart: subscribe to inputLevelStream, pass to VoiceBar.
* chore: sync Flutter build config and dependency updates
- Add Flutter migrator flags to gradle.properties (builtInKotlin, newDsl)
- Add FlutterGeneratedPluginSwiftPackage to iOS/macOS Xcode projects
- Update meta 1.17→1.18, test_api 0.7.10→0.7.11
- Rebuild chanora_bridge framework for macOS
- Update Podfile.lock for iOS and macOS
* fix(voice): correct meter animation, pre-gain dBFS, stream lifecycle, and protocol warnings
B1: Convert VoiceLevelMeter to StatefulWidget tracking previous fill
as Tween begin so the meter animates smoothly instead of resetting
to zero on every frame.
B2: Compute dBFS from pre-gain mono samples in CaptureState::ingest()
so the level meter reflects raw mic input, matching mobile paths.
B4: End input_level_stream after 10 consecutive session errors instead
of emitting -120 dBFS forever when the session is gone.
Also fixes all 13 clippy warnings in chanora_protocol: collapsed
nested if-let patterns, replaced .ok() + Some matching with Ok, used
? operator, and introduced EventChannels struct to reduce the four
helper functions below the 7-argument threshold.
* fix(voice): use MissedTickBehavior::Skip for level meter stream and align dBFS doc
Set MissedTickBehavior::Skip on the input_level_stream tokio interval
so slow audio_stats() calls skip missed ticks instead of bursting,
preventing CPU spikes on the UI meter thread.
Align VoiceLevelMeter class doc: the mapping floors at -60 dBFS
(via dbfsToFraction), not the full -120 range.
This commit is contained in:
@@ -404,6 +404,19 @@ impl Default for 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.
|
||||
pub fn update_capture(
|
||||
&self,
|
||||
|
||||
@@ -838,6 +838,7 @@ impl AudioEngine {
|
||||
transmit_flag_for_capture,
|
||||
frames_sent.clone(),
|
||||
cfg.mic_gain,
|
||||
audio_processing_stats.clone(),
|
||||
);
|
||||
let (input_stream, capture_active) = match capture_result {
|
||||
Ok(s) => (Some(s), true),
|
||||
@@ -1597,6 +1598,11 @@ impl AudioEngine {
|
||||
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.
|
||||
pub fn audio_processing_config_snapshot(&self) -> crate::AudioProcessingConfig {
|
||||
self.audio_processing_config.lock().unwrap().clone()
|
||||
@@ -1717,6 +1723,7 @@ fn try_open_capture(
|
||||
transmit_active: Arc<AtomicBool>,
|
||||
frames_sent: Arc<AtomicU32>,
|
||||
mic_gain: f32,
|
||||
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
||||
) -> Result<cpal::Stream, AudioError> {
|
||||
let in_cfg = in_dev
|
||||
.default_input_config()
|
||||
@@ -1752,6 +1759,7 @@ fn try_open_capture(
|
||||
voice_out_tx,
|
||||
transmit_active,
|
||||
frames_sent,
|
||||
audio_processing_stats,
|
||||
)));
|
||||
|
||||
let stream = match in_format {
|
||||
@@ -1803,6 +1811,7 @@ struct CaptureState {
|
||||
/// capacity so the drain-into-frame path skips the allocator
|
||||
/// after warmup. Same precedent as `mono_scratch` above.
|
||||
frame_scratch: Vec<f32>,
|
||||
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
|
||||
@@ -1815,6 +1824,7 @@ impl CaptureState {
|
||||
voice_out_tx: mpsc::Sender<OutPacket>,
|
||||
transmit_active: Arc<AtomicBool>,
|
||||
frames_sent: Arc<AtomicU32>,
|
||||
audio_processing_stats: Arc<crate::SharedAudioProcessingStats>,
|
||||
) -> Self {
|
||||
Self {
|
||||
encoder,
|
||||
@@ -1828,12 +1838,9 @@ impl CaptureState {
|
||||
voice_out_tx,
|
||||
transmit_active,
|
||||
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),
|
||||
// Exact upper bound: drain pulls FRAME_SAMPLES at a time.
|
||||
frame_scratch: Vec::with_capacity(FRAME_SAMPLES),
|
||||
audio_processing_stats,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1841,17 +1848,8 @@ impl CaptureState {
|
||||
/// 48 kHz mono frames; encode and send when `transmit_active`
|
||||
/// is true (PTT engaged).
|
||||
fn ingest<T: ToF32 + Copy>(&mut self, buf: &[T]) {
|
||||
if !self.transmit_active.load(Ordering::Relaxed) {
|
||||
// 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.
|
||||
// 1. Down-mix to mono (pre-gain). Always performed so the level
|
||||
// meter reflects real mic input even when PTT is released.
|
||||
let in_channels = self.in_channels;
|
||||
let mic_gain = self.mic_gain;
|
||||
self.mono_scratch.clear();
|
||||
@@ -1859,8 +1857,23 @@ impl CaptureState {
|
||||
self.mono_scratch.reserve(frame_count);
|
||||
for frame in buf.chunks(in_channels) {
|
||||
let sum: f32 = frame.iter().map(|s| s.to_f32_sample()).sum();
|
||||
self.mono_scratch
|
||||
.push((sum / frame.len() as f32) * mic_gain);
|
||||
self.mono_scratch.push(sum / frame.len() as f32);
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -2488,6 +2501,7 @@ pub mod bench_seam {
|
||||
tx,
|
||||
transmit_active.clone(),
|
||||
frames_sent,
|
||||
Arc::new(crate::SharedAudioProcessingStats::default()),
|
||||
);
|
||||
Self {
|
||||
state,
|
||||
|
||||
@@ -1038,6 +1038,8 @@ pub struct BridgeAudioStats {
|
||||
pub frames_received: u32,
|
||||
/// Current push-to-talk state.
|
||||
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.
|
||||
@@ -2096,7 +2098,7 @@ pub fn events_stream(sink: StreamSink<BridgeEvent>) -> Result<(), BridgeError> {
|
||||
|
||||
/// Read audio statistics. Errors if no connection or audio not started.
|
||||
pub async fn audio_stats() -> Result<BridgeAudioStats, BridgeError> {
|
||||
let (s, r, p) = runtime()
|
||||
let (s, r, p, lvl) = runtime()
|
||||
.spawn(async { session().audio_stats().await })
|
||||
.await
|
||||
.map_err(|e| task_join_error("audio_stats", e))??;
|
||||
@@ -2104,9 +2106,42 @@ pub async fn audio_stats() -> Result<BridgeAudioStats, BridgeError> {
|
||||
frames_sent: s,
|
||||
frames_received: r,
|
||||
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.
|
||||
pub async fn set_audio_processing_config(
|
||||
config: BridgeAudioProcessingConfig,
|
||||
|
||||
@@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||
);
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 281698435;
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -20394775;
|
||||
|
||||
// Section: executor
|
||||
|
||||
@@ -738,6 +738,42 @@ 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(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
@@ -1790,6 +1826,14 @@ 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 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
@@ -1962,10 +2006,12 @@ impl SseDecode for crate::api::BridgeAudioStats {
|
||||
let mut var_framesSent = <u32>::sse_decode(deserializer);
|
||||
let mut var_framesReceived = <u32>::sse_decode(deserializer);
|
||||
let mut var_pttActive = <bool>::sse_decode(deserializer);
|
||||
let mut var_inputLevel = <f32>::sse_decode(deserializer);
|
||||
return crate::api::BridgeAudioStats {
|
||||
frames_sent: var_framesSent,
|
||||
frames_received: var_framesReceived,
|
||||
ptt_active: var_pttActive,
|
||||
input_level: var_inputLevel,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2755,33 +2801,34 @@ fn pde_ffi_dispatcher_primary_impl(
|
||||
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),
|
||||
20 => wire__crate__api__init_storage_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__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),
|
||||
25 => 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__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),
|
||||
31 => 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_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_muted_impl(port, ptr, rust_vec_len, data_len),
|
||||
36 => {
|
||||
21 => wire__crate__api__input_level_stream_impl(port, ptr, rust_vec_len, data_len),
|
||||
22 => wire__crate__api__is_connected_impl(port, ptr, rust_vec_len, data_len),
|
||||
23 => wire__crate__api__list_audio_devices_impl(port, ptr, rust_vec_len, data_len),
|
||||
24 => wire__crate__api__list_bookmarks_impl(port, ptr, rust_vec_len, data_len),
|
||||
26 => wire__crate__api__move_to_channel_impl(port, ptr, rust_vec_len, data_len),
|
||||
27 => wire__crate__api__prefetch_server_impl(port, ptr, rust_vec_len, data_len),
|
||||
28 => wire__crate__api__ptt_descriptor_impl(port, ptr, rust_vec_len, data_len),
|
||||
30 => wire__crate__api__send_chat_message_impl(port, ptr, rust_vec_len, data_len),
|
||||
32 => wire__crate__api__set_audio_processing_config_impl(port, ptr, rust_vec_len, data_len),
|
||||
33 => wire__crate__api__set_client_volume_impl(port, ptr, rust_vec_len, data_len),
|
||||
34 => wire__crate__api__set_hard_mute_impl(port, ptr, rust_vec_len, data_len),
|
||||
35 => wire__crate__api__set_input_device_impl(port, ptr, rust_vec_len, data_len),
|
||||
36 => wire__crate__api__set_input_muted_impl(port, ptr, rust_vec_len, data_len),
|
||||
37 => {
|
||||
wire__crate__api__set_ios_voice_processing_mode_impl(port, ptr, rust_vec_len, data_len)
|
||||
}
|
||||
38 => wire__crate__api__set_output_device_impl(port, ptr, rust_vec_len, data_len),
|
||||
39 => wire__crate__api__set_output_gain_impl(port, ptr, rust_vec_len, data_len),
|
||||
40 => wire__crate__api__set_output_muted_impl(port, ptr, rust_vec_len, data_len),
|
||||
41 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len),
|
||||
42 => wire__crate__api__set_ptt_binding_impl(port, ptr, rust_vec_len, data_len),
|
||||
43 => wire__crate__api__set_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
|
||||
44 => wire__crate__api__set_transmit_mode_impl(port, ptr, rust_vec_len, data_len),
|
||||
45 => wire__crate__api__set_vad_model_path_impl(port, ptr, rust_vec_len, data_len),
|
||||
46 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len),
|
||||
47 => wire__crate__api__update_bookmark_impl(port, ptr, rust_vec_len, data_len),
|
||||
48 => wire__crate__api__voice_join_impl(port, ptr, rust_vec_len, data_len),
|
||||
49 => wire__crate__api__voice_leave_impl(port, ptr, rust_vec_len, data_len),
|
||||
39 => 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),
|
||||
41 => 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),
|
||||
43 => 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),
|
||||
45 => 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),
|
||||
47 => 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),
|
||||
49 => 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),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -2803,10 +2850,10 @@ fn pde_ffi_dispatcher_sync_impl(
|
||||
data_len,
|
||||
),
|
||||
19 => wire__crate__api__handle_route_change_impl(ptr, rust_vec_len, data_len),
|
||||
24 => wire__crate__api__log_file_path_str_impl(ptr, rust_vec_len, data_len),
|
||||
28 => wire__crate__api__record_lifecycle_event_impl(ptr, rust_vec_len, data_len),
|
||||
30 => wire__crate__api__set_audio_output_route_impl(ptr, rust_vec_len, data_len),
|
||||
37 => wire__crate__api__set_network_state_impl(ptr, rust_vec_len, data_len),
|
||||
25 => 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),
|
||||
31 => 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),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -2984,6 +3031,7 @@ impl flutter_rust_bridge::IntoDart for crate::api::BridgeAudioStats {
|
||||
self.frames_sent.into_into_dart().into_dart(),
|
||||
self.frames_received.into_into_dart().into_dart(),
|
||||
self.ptt_active.into_into_dart().into_dart(),
|
||||
self.input_level.into_into_dart().into_dart(),
|
||||
]
|
||||
.into_dart()
|
||||
}
|
||||
@@ -3652,6 +3700,13 @@ 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 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
@@ -3781,6 +3836,7 @@ impl SseEncode for crate::api::BridgeAudioStats {
|
||||
<u32>::sse_encode(self.frames_sent, serializer);
|
||||
<u32>::sse_encode(self.frames_received, serializer);
|
||||
<bool>::sse_encode(self.ptt_active, serializer);
|
||||
<f32>::sse_encode(self.input_level, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,13 @@ 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)]
|
||||
enum SendTimeoutError<T> {
|
||||
Timeout(T),
|
||||
@@ -278,10 +285,12 @@ impl ProtocolClient {
|
||||
cfg.clone(),
|
||||
rx,
|
||||
voice_out_rx,
|
||||
voice_in_tx,
|
||||
chat_tx,
|
||||
activity_tx,
|
||||
delta_tx,
|
||||
EventChannels {
|
||||
voice_in: voice_in_tx,
|
||||
chat: chat_tx,
|
||||
activity: activity_tx,
|
||||
delta: delta_tx,
|
||||
},
|
||||
ready_tx,
|
||||
lost_tx,
|
||||
));
|
||||
@@ -502,10 +511,7 @@ async fn connection_task(
|
||||
cfg: ConnectConfig,
|
||||
mut rx: mpsc::Receiver<Request>,
|
||||
mut voice_out_rx: mpsc::Receiver<OutPacket>,
|
||||
voice_in_tx: mpsc::Sender<InboundVoice>,
|
||||
chat_tx: mpsc::Sender<ChatMessage>,
|
||||
activity_tx: mpsc::Sender<ServerActivity>,
|
||||
delta_tx: mpsc::Sender<ProtocolDelta>,
|
||||
channels: EventChannels,
|
||||
ready_tx: oneshot::Sender<Result<(), ProtocolError>>,
|
||||
lost_tx: oneshot::Sender<DisconnectReason>,
|
||||
) {
|
||||
@@ -686,14 +692,14 @@ async fn connection_task(
|
||||
Ok(Some(Ok(item))) => {
|
||||
match item {
|
||||
StreamItem::Audio(buf) => {
|
||||
handle_audio_stream_item(&voice_in_tx, &mut voice_activity, buf).await;
|
||||
handle_audio_stream_item(&channels.voice_in, &mut voice_activity, buf).await;
|
||||
}
|
||||
other => handle_non_audio_stream_item(
|
||||
&con,
|
||||
other,
|
||||
&chat_tx,
|
||||
&activity_tx,
|
||||
&delta_tx,
|
||||
&channels.chat,
|
||||
&channels.activity,
|
||||
&channels.delta,
|
||||
&mut pending_moves,
|
||||
),
|
||||
}
|
||||
@@ -730,10 +736,8 @@ async fn connection_task(
|
||||
})
|
||||
.collect();
|
||||
for handle in expired {
|
||||
if let Some((_target_channel, reply, _)) = pending_moves.remove(&handle) {
|
||||
if let Some(reply) = reply {
|
||||
let _ = reply.send(Ok(()));
|
||||
}
|
||||
if let Some((_target_channel, Some(reply), _)) = pending_moves.remove(&handle) {
|
||||
let _ = reply.send(Ok(()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -794,10 +798,7 @@ async fn connection_task(
|
||||
let r = fetch_client_profile(
|
||||
&mut con,
|
||||
client_id,
|
||||
&voice_in_tx,
|
||||
&chat_tx,
|
||||
&activity_tx,
|
||||
&delta_tx,
|
||||
&channels,
|
||||
&mut pending_moves,
|
||||
&mut voice_activity,
|
||||
)
|
||||
@@ -872,7 +873,7 @@ fn handle_non_audio_stream_item(
|
||||
} = &ev
|
||||
{
|
||||
let own_client = con.get_state().ok().map(|state| state.own_client);
|
||||
if let Some(state) = con.get_state().ok() {
|
||||
if let Ok(state) = con.get_state() {
|
||||
if let Some(client) = state.clients.get(client_id) {
|
||||
let _ = delta_tx.try_send(ProtocolDelta::ClientMoved {
|
||||
client_id: client_id.0 as u64,
|
||||
@@ -1118,10 +1119,7 @@ fn send_text_to_mode(
|
||||
async fn fetch_client_profile(
|
||||
con: &mut Connection,
|
||||
client_id: u64,
|
||||
voice_in_tx: &mpsc::Sender<InboundVoice>,
|
||||
chat_tx: &mpsc::Sender<ChatMessage>,
|
||||
activity_tx: &mpsc::Sender<ServerActivity>,
|
||||
delta_tx: &mpsc::Sender<ProtocolDelta>,
|
||||
channels: &EventChannels,
|
||||
pending_moves: &mut PendingMoves,
|
||||
voice_activity: &mut HashMap<u64, Instant>,
|
||||
) -> Result<ClientProfile, ProtocolError> {
|
||||
@@ -1158,10 +1156,7 @@ async fn fetch_client_profile(
|
||||
let _ = request_messages(
|
||||
con,
|
||||
build_command("servergrouplist", &[], &[]),
|
||||
voice_in_tx,
|
||||
chat_tx,
|
||||
activity_tx,
|
||||
delta_tx,
|
||||
channels,
|
||||
pending_moves,
|
||||
voice_activity,
|
||||
)
|
||||
@@ -1171,10 +1166,7 @@ async fn fetch_client_profile(
|
||||
let _ = request_messages(
|
||||
con,
|
||||
build_command("channelgrouplist", &[], &[]),
|
||||
voice_in_tx,
|
||||
chat_tx,
|
||||
activity_tx,
|
||||
delta_tx,
|
||||
channels,
|
||||
pending_moves,
|
||||
voice_activity,
|
||||
)
|
||||
@@ -1188,10 +1180,7 @@ async fn fetch_client_profile(
|
||||
&[("clid", client_id.to_string())],
|
||||
&[],
|
||||
),
|
||||
voice_in_tx,
|
||||
chat_tx,
|
||||
activity_tx,
|
||||
delta_tx,
|
||||
channels,
|
||||
pending_moves,
|
||||
voice_activity,
|
||||
)
|
||||
@@ -1209,10 +1198,7 @@ async fn fetch_client_profile(
|
||||
if let Err(e) = request_messages(
|
||||
con,
|
||||
build_command("getconnectioninfo", &[("clid", client_id.to_string())], &[]),
|
||||
voice_in_tx,
|
||||
chat_tx,
|
||||
activity_tx,
|
||||
delta_tx,
|
||||
channels,
|
||||
pending_moves,
|
||||
voice_activity,
|
||||
)
|
||||
@@ -1231,10 +1217,7 @@ async fn fetch_client_profile(
|
||||
request_client_db_info(
|
||||
con,
|
||||
database_id,
|
||||
voice_in_tx,
|
||||
chat_tx,
|
||||
activity_tx,
|
||||
delta_tx,
|
||||
channels,
|
||||
pending_moves,
|
||||
voice_activity,
|
||||
)
|
||||
@@ -1392,10 +1375,7 @@ fn client_profile_refresh_plan(
|
||||
async fn request_messages(
|
||||
con: &mut Connection,
|
||||
command: OutCommand,
|
||||
voice_in_tx: &mpsc::Sender<InboundVoice>,
|
||||
chat_tx: &mpsc::Sender<ChatMessage>,
|
||||
activity_tx: &mpsc::Sender<ServerActivity>,
|
||||
delta_tx: &mpsc::Sender<ProtocolDelta>,
|
||||
channels: &EventChannels,
|
||||
pending_moves: &mut PendingMoves,
|
||||
voice_activity: &mut HashMap<u64, Instant>,
|
||||
) -> Result<Vec<InMessage>, ProtocolError> {
|
||||
@@ -1431,14 +1411,14 @@ async fn request_messages(
|
||||
return Ok(messages);
|
||||
}
|
||||
StreamItem::Audio(buf) => {
|
||||
handle_audio_stream_item(voice_in_tx, voice_activity, buf).await;
|
||||
handle_audio_stream_item(&channels.voice_in, voice_activity, buf).await;
|
||||
}
|
||||
other => handle_non_audio_stream_item(
|
||||
con,
|
||||
other,
|
||||
chat_tx,
|
||||
activity_tx,
|
||||
delta_tx,
|
||||
&channels.chat,
|
||||
&channels.activity,
|
||||
&channels.delta,
|
||||
pending_moves,
|
||||
),
|
||||
}
|
||||
@@ -1448,20 +1428,14 @@ async fn request_messages(
|
||||
async fn request_client_db_info(
|
||||
con: &mut Connection,
|
||||
dbid: tsclientlib::ClientDbId,
|
||||
voice_in_tx: &mpsc::Sender<InboundVoice>,
|
||||
chat_tx: &mpsc::Sender<ChatMessage>,
|
||||
activity_tx: &mpsc::Sender<ServerActivity>,
|
||||
delta_tx: &mpsc::Sender<ProtocolDelta>,
|
||||
channels: &EventChannels,
|
||||
pending_moves: &mut PendingMoves,
|
||||
voice_activity: &mut HashMap<u64, Instant>,
|
||||
) -> Result<InClientDbInfoPart, ProtocolError> {
|
||||
let messages = request_messages(
|
||||
con,
|
||||
build_command("clientdbinfo", &[("cldbid", dbid.0.to_string())], &[]),
|
||||
voice_in_tx,
|
||||
chat_tx,
|
||||
activity_tx,
|
||||
delta_tx,
|
||||
channels,
|
||||
pending_moves,
|
||||
voice_activity,
|
||||
)
|
||||
@@ -1784,9 +1758,7 @@ fn format_server_activity(con: &Connection, ev: &tsclientlib::events::Event) ->
|
||||
extra,
|
||||
..
|
||||
} => {
|
||||
if extra.reason.is_none() {
|
||||
return None;
|
||||
}
|
||||
extra.reason?;
|
||||
let client = activity_client(con, *client_id)?;
|
||||
let channel = activity_channel_name(con, client.channel)?;
|
||||
Some(format!(
|
||||
@@ -2164,7 +2136,7 @@ fn forward_delta(
|
||||
id: PropertyId::Client(client_id),
|
||||
..
|
||||
} => {
|
||||
if let Some(state) = con.get_state().ok() {
|
||||
if let Ok(state) = con.get_state() {
|
||||
if let Some(client) = state.clients.get(client_id) {
|
||||
let _ = delta_tx.try_send(ProtocolDelta::ClientJoined {
|
||||
client_id: client_id.0 as u64,
|
||||
@@ -2181,15 +2153,13 @@ fn forward_delta(
|
||||
}
|
||||
Event::PropertyRemoved {
|
||||
id: PropertyId::Client(_),
|
||||
old,
|
||||
old: PropertyValue::Client(client),
|
||||
..
|
||||
} => {
|
||||
if let PropertyValue::Client(client) = old {
|
||||
let _ = delta_tx.try_send(ProtocolDelta::ClientLeft {
|
||||
client_id: client.id.0 as u64,
|
||||
name: client.name.clone(),
|
||||
});
|
||||
}
|
||||
let _ = delta_tx.try_send(ProtocolDelta::ClientLeft {
|
||||
client_id: client.id.0 as u64,
|
||||
name: client.name.clone(),
|
||||
});
|
||||
}
|
||||
Event::PropertyChanged {
|
||||
id: PropertyId::ClientChannel(_),
|
||||
@@ -2199,7 +2169,7 @@ fn forward_delta(
|
||||
id: PropertyId::Client(client_id),
|
||||
..
|
||||
} => {
|
||||
if let Some(state) = con.get_state().ok() {
|
||||
if let Ok(state) = con.get_state() {
|
||||
if let Some(client) = state.clients.get(client_id) {
|
||||
let _ = delta_tx.try_send(ProtocolDelta::ClientUpdated {
|
||||
client_id: client_id.0 as u64,
|
||||
@@ -2216,7 +2186,7 @@ fn forward_delta(
|
||||
id: PropertyId::Channel(channel_id),
|
||||
..
|
||||
} => {
|
||||
if let Some(state) = con.get_state().ok() {
|
||||
if let Ok(state) = con.get_state() {
|
||||
if let Some(channel) = state.channels.get(channel_id) {
|
||||
let _ = delta_tx.try_send(ProtocolDelta::ChannelAdded {
|
||||
id: channel_id.0,
|
||||
@@ -2231,20 +2201,18 @@ fn forward_delta(
|
||||
}
|
||||
Event::PropertyRemoved {
|
||||
id: PropertyId::Channel(_),
|
||||
old,
|
||||
old: PropertyValue::Channel(channel),
|
||||
..
|
||||
} => {
|
||||
if let PropertyValue::Channel(channel) = old {
|
||||
let _ = delta_tx.try_send(ProtocolDelta::ChannelRemoved {
|
||||
id: channel.id.0,
|
||||
});
|
||||
}
|
||||
let _ = delta_tx.try_send(ProtocolDelta::ChannelRemoved {
|
||||
id: channel.id.0,
|
||||
});
|
||||
}
|
||||
Event::PropertyChanged {
|
||||
id: PropertyId::Channel(channel_id),
|
||||
..
|
||||
} => {
|
||||
if let Some(state) = con.get_state().ok() {
|
||||
if let Ok(state) = con.get_state() {
|
||||
if let Some(channel) = state.channels.get(channel_id) {
|
||||
let _ = delta_tx.try_send(ProtocolDelta::ChannelUpdated {
|
||||
id: channel_id.0,
|
||||
|
||||
Reference in New Issue
Block a user