fix: refine linux voice runtime behavior
This commit is contained in:
@@ -198,7 +198,8 @@ Future<void> sendChatMessage({
|
|||||||
/// On non-Android targets or before a voice session opens the
|
/// On non-Android targets or before a voice session opens the
|
||||||
/// section is omitted. Per SDD-090 every field in that section is
|
/// section is omitted. Per SDD-090 every field in that section is
|
||||||
/// a device-side technical scalar — no PII admitted.
|
/// a device-side technical scalar — no PII admitted.
|
||||||
String exportDiagnostics() => RustLib.instance.api.crateApiExportDiagnostics();
|
Future<String> exportDiagnostics() =>
|
||||||
|
RustLib.instance.api.crateApiExportDiagnostics();
|
||||||
|
|
||||||
/// Wire the identity persistence store to a platform-private
|
/// Wire the identity persistence store to a platform-private
|
||||||
/// directory. Should be called once on app start after Flutter has
|
/// directory. Should be called once on app start after Flutter has
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ abstract class RustLibApi extends BaseApi {
|
|||||||
|
|
||||||
Stream<BridgeEvent> crateApiEventsStream();
|
Stream<BridgeEvent> crateApiEventsStream();
|
||||||
|
|
||||||
String crateApiExportDiagnostics();
|
Future<String> crateApiExportDiagnostics();
|
||||||
|
|
||||||
Future<BridgeAudioProcessingConfig> crateApiGetAudioProcessingConfig();
|
Future<BridgeAudioProcessingConfig> crateApiGetAudioProcessingConfig();
|
||||||
|
|
||||||
@@ -469,16 +469,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
const TaskConstMeta(debugName: "events_stream", argNames: ["sink"]);
|
const TaskConstMeta(debugName: "events_stream", argNames: ["sink"]);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String crateApiExportDiagnostics() {
|
Future<String> crateApiExportDiagnostics() {
|
||||||
return handler.executeSync(
|
return handler.executeNormal(
|
||||||
SyncTask(
|
NormalTask(
|
||||||
callFfi: () {
|
callFfi: (port_) {
|
||||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 10)!;
|
pdeCallFfi(
|
||||||
|
generalizedFrbRustBinding,
|
||||||
|
serializer,
|
||||||
|
funcId: 10,
|
||||||
|
port: port_,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
codec: SseCodec(
|
codec: SseCodec(
|
||||||
decodeSuccessData: sse_decode_String,
|
decodeSuccessData: sse_decode_String,
|
||||||
decodeErrorData: null,
|
decodeErrorData: sse_decode_bridge_error,
|
||||||
),
|
),
|
||||||
constMeta: kCrateApiExportDiagnosticsConstMeta,
|
constMeta: kCrateApiExportDiagnosticsConstMeta,
|
||||||
argValues: [],
|
argValues: [],
|
||||||
|
|||||||
@@ -1234,11 +1234,11 @@ impl ChanoraSession {
|
|||||||
audio.set_output_muted(muted);
|
audio.set_output_muted(muted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Input mute must also stop local outbound voice production
|
// Muting the speaker is treated as a local deafen: it must
|
||||||
// so the transmit selector stays in sync with the server-side
|
// also clamp microphone TX so users cannot keep talking while
|
||||||
// mic mute. Output mute is playback-only and must not affect
|
// unable to hear replies. This does not send server-side
|
||||||
// the mic gate.
|
// input-mute; it only drives the local transmit gate.
|
||||||
let mic_disabled = state.local_input_muted;
|
let mic_disabled = state.local_input_muted || state.local_output_muted;
|
||||||
self.voice_selector.set_hard_mute(mic_disabled);
|
self.voice_selector.set_hard_mute(mic_disabled);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1774,11 +1774,11 @@ impl AudioEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Latest Android voice-audio diagnostics snapshot (SDD-112 item
|
/// Latest Android voice-audio diagnostics snapshot (SDD-112 item
|
||||||
/// 10 / SDD-113 item 7 / SDD-116 item 3). On non-Android targets
|
/// 10 / SDD-113 item 7 / SDD-116 item 3). Returns `Some(...)`
|
||||||
/// this always returns `None`. On Android it returns `Some(...)`
|
|
||||||
/// once `AndroidVoiceUnit::open()` has published a snapshot; the
|
/// once `AndroidVoiceUnit::open()` has published a snapshot; the
|
||||||
/// slot is cleared on `close()` / `Drop`. Per SDD-090 the
|
/// slot is cleared on `close()` / `Drop`. Per SDD-090 the snapshot
|
||||||
/// snapshot contains only device-side technical scalars — no PII.
|
/// contains only device-side technical scalars — no PII.
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
pub fn android_diagnostics(
|
pub fn android_diagnostics(
|
||||||
&self,
|
&self,
|
||||||
) -> Option<crate::mobile_voice_backend::AndroidAudioDiagnostics> {
|
) -> Option<crate::mobile_voice_backend::AndroidAudioDiagnostics> {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
//! ## What's wired in this Beta
|
//! ## What's wired in this Beta
|
||||||
//!
|
//!
|
||||||
//! * Default-input capture via platform audio backends (Oboe on Android,
|
//! * Default-input capture via platform audio backends (Oboe on Android,
|
||||||
//! VoiceProcessingIO on Apple platforms, cpal/SDL elsewhere)
|
//! VoiceProcessingIO on Apple platforms, PipeWire/PulseAudio on Linux, cpal on Windows)
|
||||||
//! * Frame-aligned 20 ms / 48 kHz mono Opus encoding via `audiopus`
|
//! * Frame-aligned 20 ms / 48 kHz mono Opus encoding via `audiopus`
|
||||||
//! * Forward encoded frames to the protocol crate as `OutPacket`s
|
//! * Forward encoded frames to the protocol crate as `OutPacket`s
|
||||||
//! * Inbound voice packets fed to `tsclientlib::audio::AudioHandler`
|
//! * Inbound voice packets fed to `tsclientlib::audio::AudioHandler`
|
||||||
@@ -32,6 +32,7 @@ pub mod audio_processing;
|
|||||||
pub mod debug_wav;
|
pub mod debug_wav;
|
||||||
mod engine;
|
mod engine;
|
||||||
pub mod frame;
|
pub mod frame;
|
||||||
|
#[cfg(any(target_os = "android", target_os = "ios", target_os = "macos"))]
|
||||||
pub mod mobile_voice_backend;
|
pub mod mobile_voice_backend;
|
||||||
pub mod mode_stack;
|
pub mod mode_stack;
|
||||||
pub(crate) mod opus_voice;
|
pub(crate) mod opus_voice;
|
||||||
@@ -44,6 +45,7 @@ pub mod transmit_mode;
|
|||||||
pub mod transmit_selector;
|
pub mod transmit_selector;
|
||||||
pub mod vad;
|
pub mod vad;
|
||||||
pub mod voice_activity;
|
pub mod voice_activity;
|
||||||
|
#[cfg(any(target_os = "android", target_os = "ios", test))]
|
||||||
pub(crate) mod voice_render;
|
pub(crate) mod voice_render;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ pub(crate) fn input_device_id(node_id: u32) -> String {
|
|||||||
format!("{INPUT_ID_PREFIX}{node_id}")
|
format!("{INPUT_ID_PREFIX}{node_id}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
pub(crate) fn input_device_node_id_from_id(id: &str) -> Option<u32> {
|
pub(crate) fn input_device_node_id_from_id(id: &str) -> Option<u32> {
|
||||||
id.strip_prefix(INPUT_ID_PREFIX)?.parse().ok()
|
id.strip_prefix(INPUT_ID_PREFIX)?.parse().ok()
|
||||||
}
|
}
|
||||||
@@ -99,6 +100,7 @@ pub(crate) fn output_device_id(node_id: u32) -> String {
|
|||||||
format!("{OUTPUT_ID_PREFIX}{node_id}")
|
format!("{OUTPUT_ID_PREFIX}{node_id}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
pub(crate) fn output_device_node_id_from_id(id: &str) -> Option<u32> {
|
pub(crate) fn output_device_node_id_from_id(id: &str) -> Option<u32> {
|
||||||
id.strip_prefix(OUTPUT_ID_PREFIX)?.parse().ok()
|
id.strip_prefix(OUTPUT_ID_PREFIX)?.parse().ok()
|
||||||
}
|
}
|
||||||
@@ -111,23 +113,25 @@ fn pulse_output_device_id(name: &str) -> String {
|
|||||||
format!("{PULSE_OUTPUT_ID_PREFIX}{name}")
|
format!("{PULSE_OUTPUT_ID_PREFIX}{name}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
fn pulse_input_name_from_id(id: &str) -> Option<&str> {
|
fn pulse_input_name_from_id(id: &str) -> Option<&str> {
|
||||||
id.strip_prefix(PULSE_INPUT_ID_PREFIX)
|
id.strip_prefix(PULSE_INPUT_ID_PREFIX)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
fn pulse_output_name_from_id(id: &str) -> Option<&str> {
|
fn pulse_output_name_from_id(id: &str) -> Option<&str> {
|
||||||
id.strip_prefix(PULSE_OUTPUT_ID_PREFIX)
|
id.strip_prefix(PULSE_OUTPUT_ID_PREFIX)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn list_input_devices(default_label: Option<&str>) -> Vec<AudioDeviceInfo> {
|
pub(crate) fn list_input_devices(default_label: Option<&str>) -> Vec<AudioDeviceInfo> {
|
||||||
list_devices(LinuxDirection::Input)
|
visible_devices(list_devices(LinuxDirection::Input))
|
||||||
.iter()
|
.iter()
|
||||||
.map(|device| device.to_audio_device_info(default_label))
|
.map(|device| device.to_audio_device_info(default_label))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn list_output_devices(default_label: Option<&str>) -> Vec<AudioDeviceInfo> {
|
pub(crate) fn list_output_devices(default_label: Option<&str>) -> Vec<AudioDeviceInfo> {
|
||||||
list_devices(LinuxDirection::Output)
|
visible_devices(list_devices(LinuxDirection::Output))
|
||||||
.iter()
|
.iter()
|
||||||
.map(|device| device.to_audio_device_info(default_label))
|
.map(|device| device.to_audio_device_info(default_label))
|
||||||
.collect()
|
.collect()
|
||||||
@@ -249,6 +253,25 @@ fn list_devices(direction: LinuxDirection) -> Vec<LinuxDevice> {
|
|||||||
devices
|
devices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn visible_devices(devices: Vec<LinuxDevice>) -> Vec<LinuxDevice> {
|
||||||
|
let pipewire_labels = devices
|
||||||
|
.iter()
|
||||||
|
.filter(|device| device.backend == LinuxBackend::PipeWire)
|
||||||
|
.map(|device| normalized_device_label(&device.label))
|
||||||
|
.collect::<std::collections::HashSet<_>>();
|
||||||
|
devices
|
||||||
|
.into_iter()
|
||||||
|
.filter(|device| {
|
||||||
|
device.backend == LinuxBackend::PipeWire
|
||||||
|
|| !pipewire_labels.contains(&normalized_device_label(&device.label))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalized_device_label(label: &str) -> String {
|
||||||
|
label.split_whitespace().collect::<Vec<_>>().join(" ").to_lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
fn list_pipewire_nodes(direction: LinuxDirection) -> Result<Vec<LinuxDevice>, String> {
|
fn list_pipewire_nodes(direction: LinuxDirection) -> Result<Vec<LinuxDevice>, String> {
|
||||||
pw::init();
|
pw::init();
|
||||||
let mainloop = pw::main_loop::MainLoopRc::new(None).map_err(|err| err.to_string())?;
|
let mainloop = pw::main_loop::MainLoopRc::new(None).map_err(|err| err.to_string())?;
|
||||||
@@ -928,7 +951,8 @@ mod tests {
|
|||||||
use super::{
|
use super::{
|
||||||
input_device_id, input_device_node_id_from_id, output_device_id,
|
input_device_id, input_device_node_id_from_id, output_device_id,
|
||||||
output_device_node_id_from_id, pulse_input_device_id, pulse_input_name_from_id,
|
output_device_node_id_from_id, pulse_input_device_id, pulse_input_name_from_id,
|
||||||
pulse_output_device_id, pulse_output_name_from_id,
|
pulse_output_device_id, pulse_output_name_from_id, visible_devices, LinuxBackend,
|
||||||
|
LinuxDevice, LinuxDirection,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -958,4 +982,32 @@ mod tests {
|
|||||||
Some("alsa_output.pci-0000_00_1f.3")
|
Some("alsa_output.pci-0000_00_1f.3")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn visible_devices_hides_pulse_duplicates_when_pipewire_matches() {
|
||||||
|
let devices = visible_devices(vec![
|
||||||
|
LinuxDevice {
|
||||||
|
backend: LinuxBackend::PipeWire,
|
||||||
|
direction: LinuxDirection::Output,
|
||||||
|
raw_id: "42".to_string(),
|
||||||
|
label: "Built-in Audio Analog Stereo".to_string(),
|
||||||
|
},
|
||||||
|
LinuxDevice {
|
||||||
|
backend: LinuxBackend::PulseAudio,
|
||||||
|
direction: LinuxDirection::Output,
|
||||||
|
raw_id: "alsa_output.pci-0000".to_string(),
|
||||||
|
label: " built-in audio analog stereo ".to_string(),
|
||||||
|
},
|
||||||
|
LinuxDevice {
|
||||||
|
backend: LinuxBackend::PulseAudio,
|
||||||
|
direction: LinuxDirection::Output,
|
||||||
|
raw_id: "bluez_output.headset".to_string(),
|
||||||
|
label: "Headset".to_string(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(devices.len(), 2);
|
||||||
|
assert!(devices.iter().any(|device| device.backend == LinuxBackend::PipeWire));
|
||||||
|
assert!(devices.iter().any(|device| device.label == "Headset"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,13 @@
|
|||||||
//! * `try_select` runs a synchronous portal-version probe on
|
//! * `try_select` runs a synchronous portal-version probe on
|
||||||
//! the blocking proxy. Failure → `None` → Focused fallback.
|
//! the blocking proxy. Failure → `None` → Focused fallback.
|
||||||
//! * `start(gate, binding)` spawns a worker tokio task that
|
//! * `start(gate, binding)` spawns a worker tokio task that
|
||||||
//! owns an async `zbus::Connection`, calls `CreateSession`,
|
//! owns an async `zbus::Connection` and calls `CreateSession`.
|
||||||
//! then `BindShortcuts` (sentinel id `"chanora-ptt"`). The
|
//! If a binding was already persisted, it also calls
|
||||||
//! portal opens its own system dialog asking the user to
|
//! `BindShortcuts` (sentinel id `"chanora-ptt"`). Otherwise
|
||||||
//! pick a key; while the dialog is open the engine continues
|
//! `BindShortcuts` waits for the user's explicit Configure
|
||||||
//! at L0Focused — the audio path is not blocked.
|
//! action, which is when the portal opens its system dialog.
|
||||||
|
//! While the dialog is open the engine continues at L0Focused —
|
||||||
|
//! the audio path is not blocked.
|
||||||
//! * Once the user accepts, the task subscribes to
|
//! * Once the user accepts, the task subscribes to
|
||||||
//! `Activated`/`Deactivated` signals scoped to the session
|
//! `Activated`/`Deactivated` signals scoped to the session
|
||||||
//! handle and calls `gate.set(true/false)` accordingly.
|
//! handle and calls `gate.set(true/false)` accordingly.
|
||||||
@@ -219,7 +221,7 @@ struct BackendInner {
|
|||||||
|
|
||||||
enum WorkerCmd {
|
enum WorkerCmd {
|
||||||
/// Rebind: re-issue `BindShortcuts` on the same session.
|
/// Rebind: re-issue `BindShortcuts` on the same session.
|
||||||
Rebind,
|
Bind,
|
||||||
/// Stop: close the session and exit.
|
/// Stop: close the session and exit.
|
||||||
Stop,
|
Stop,
|
||||||
}
|
}
|
||||||
@@ -296,9 +298,10 @@ impl DesktopPttBackend for LinuxGnomeWaylandBackend {
|
|||||||
// the bridge's tokio runtime, so this is satisfied.
|
// the bridge's tokio runtime, so this is satisfied.
|
||||||
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
|
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
|
||||||
let desc_tx = self.desc_tx.clone();
|
let desc_tx = self.desc_tx.clone();
|
||||||
|
let bind_on_start = binding.input_class != PttInputClass::None;
|
||||||
let class_hint = binding.input_class;
|
let class_hint = binding.input_class;
|
||||||
let worker = tokio::spawn(async move {
|
let worker = tokio::spawn(async move {
|
||||||
run_worker(gate, cmd_rx, desc_tx, class_hint).await;
|
run_worker(gate, cmd_rx, desc_tx, class_hint, bind_on_start).await;
|
||||||
});
|
});
|
||||||
// Stash command + worker handles. `try_lock` is fine: the
|
// Stash command + worker handles. `try_lock` is fine: the
|
||||||
// backend isn't yet shared, and `start` is called once at
|
// backend isn't yet shared, and `start` is called once at
|
||||||
@@ -350,7 +353,7 @@ impl DesktopPttBackend for LinuxGnomeWaylandBackend {
|
|||||||
// only — the portal decides the actual binding.
|
// only — the portal decides the actual binding.
|
||||||
if let Ok(inner) = self.inner.try_lock() {
|
if let Ok(inner) = self.inner.try_lock() {
|
||||||
if let Some(tx) = inner.cmd_tx.as_ref() {
|
if let Some(tx) = inner.cmd_tx.as_ref() {
|
||||||
let _ = tx.send(WorkerCmd::Rebind);
|
let _ = tx.send(WorkerCmd::Bind);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,6 +376,7 @@ async fn run_worker(
|
|||||||
mut cmd_rx: mpsc::UnboundedReceiver<WorkerCmd>,
|
mut cmd_rx: mpsc::UnboundedReceiver<WorkerCmd>,
|
||||||
desc_tx: watch::Sender<PttBackendDescriptor>,
|
desc_tx: watch::Sender<PttBackendDescriptor>,
|
||||||
initial_class_hint: PttInputClass,
|
initial_class_hint: PttInputClass,
|
||||||
|
bind_on_start: bool,
|
||||||
) {
|
) {
|
||||||
// Open an async D-Bus session connection. If this fails we
|
// Open an async D-Bus session connection. If this fails we
|
||||||
// emit a warning and exit; the descriptor stays at L0Focused
|
// emit a warning and exit; the descriptor stays at L0Focused
|
||||||
@@ -417,26 +421,13 @@ async fn run_worker(
|
|||||||
"linux ptt: portal session created"
|
"linux ptt: portal session created"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Bind the initial shortcut. The portal opens its own dialog.
|
if bind_on_start {
|
||||||
match bind_shortcut(&proxy, &conn, &session_handle).await {
|
run_bind_shortcut(&proxy, &conn, &session_handle, &desc_tx, initial_class_hint).await;
|
||||||
Ok(class) => publish_bound(&desc_tx, class.unwrap_or(initial_class_hint)),
|
} else {
|
||||||
Err(BindError::Cancelled) => {
|
info!(
|
||||||
warn!(
|
target: "chanora_audio",
|
||||||
target: "chanora_audio",
|
"linux ptt: portal session ready; waiting for explicit BindShortcuts request"
|
||||||
bind_status = "cancelled",
|
);
|
||||||
"linux ptt: user cancelled BindShortcuts; descriptor stays at L0Focused"
|
|
||||||
);
|
|
||||||
publish_l0(&desc_tx);
|
|
||||||
}
|
|
||||||
Err(BindError::Failed(e)) => {
|
|
||||||
warn!(
|
|
||||||
target: "chanora_audio",
|
|
||||||
bind_status = "failed",
|
|
||||||
error = %e,
|
|
||||||
"linux ptt: BindShortcuts failed; descriptor stays at L0Focused"
|
|
||||||
);
|
|
||||||
publish_l0(&desc_tx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to Activated / Deactivated signals scoped to the
|
// Subscribe to Activated / Deactivated signals scoped to the
|
||||||
@@ -468,12 +459,8 @@ async fn run_worker(
|
|||||||
tokio::select! {
|
tokio::select! {
|
||||||
cmd = cmd_rx.recv() => {
|
cmd = cmd_rx.recv() => {
|
||||||
match cmd {
|
match cmd {
|
||||||
Some(WorkerCmd::Rebind) => {
|
Some(WorkerCmd::Bind) => {
|
||||||
match bind_shortcut(&proxy, &conn, &session_handle).await {
|
run_bind_shortcut(&proxy, &conn, &session_handle, &desc_tx, initial_class_hint).await;
|
||||||
Ok(class) => publish_bound(&desc_tx, class.unwrap_or(initial_class_hint)),
|
|
||||||
Err(BindError::Cancelled) => publish_l0(&desc_tx),
|
|
||||||
Err(BindError::Failed(_)) => publish_l0(&desc_tx),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Some(WorkerCmd::Stop) | None => {
|
Some(WorkerCmd::Stop) | None => {
|
||||||
// Close the portal session via the
|
// Close the portal session via the
|
||||||
@@ -586,6 +573,35 @@ async fn bind_shortcut(
|
|||||||
Ok(class)
|
Ok(class)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn run_bind_shortcut(
|
||||||
|
proxy: &GlobalShortcutsProxy<'_>,
|
||||||
|
conn: &AsyncConnection,
|
||||||
|
session_handle: &zbus::zvariant::OwnedObjectPath,
|
||||||
|
desc_tx: &watch::Sender<PttBackendDescriptor>,
|
||||||
|
class_hint: PttInputClass,
|
||||||
|
) {
|
||||||
|
match bind_shortcut(proxy, conn, session_handle).await {
|
||||||
|
Ok(class) => publish_bound(desc_tx, class.unwrap_or(class_hint)),
|
||||||
|
Err(BindError::Cancelled) => {
|
||||||
|
warn!(
|
||||||
|
target: "chanora_audio",
|
||||||
|
bind_status = "cancelled",
|
||||||
|
"linux ptt: user cancelled BindShortcuts; descriptor stays at L0Focused"
|
||||||
|
);
|
||||||
|
publish_l0(desc_tx);
|
||||||
|
}
|
||||||
|
Err(BindError::Failed(e)) => {
|
||||||
|
warn!(
|
||||||
|
target: "chanora_audio",
|
||||||
|
bind_status = "failed",
|
||||||
|
error = %e,
|
||||||
|
"linux ptt: BindShortcuts failed; descriptor stays at L0Focused"
|
||||||
|
);
|
||||||
|
publish_l0(desc_tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Wait for the `Response` signal on `request_path`. Returns the
|
/// Wait for the `Response` signal on `request_path`. Returns the
|
||||||
/// `results` dict on success (response code 0) or an Error
|
/// `results` dict on success (response code 0) or an Error
|
||||||
/// otherwise.
|
/// otherwise.
|
||||||
|
|||||||
@@ -816,10 +816,9 @@ pub async fn set_input_muted(muted: bool) -> Result<(), BridgeError> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle self output-mute (speaker). Mutes locally *and* informs
|
/// Toggle self output-mute (speaker). Mutes locally, informs the
|
||||||
/// the server. The server uses this for the channel icon next to
|
/// server, and clamps local microphone transmit while speaker mute
|
||||||
/// the client name; the local mute kicks in immediately even
|
/// is active so the user cannot continue talking while deafened.
|
||||||
/// before the server acknowledges.
|
|
||||||
pub async fn set_output_muted(muted: bool) -> Result<(), BridgeError> {
|
pub async fn set_output_muted(muted: bool) -> Result<(), BridgeError> {
|
||||||
runtime()
|
runtime()
|
||||||
.spawn(async move { session().set_self_muted(None, Some(muted)).await })
|
.spawn(async move { session().set_self_muted(None, Some(muted)).await })
|
||||||
@@ -1230,8 +1229,7 @@ impl From<chanora_core::AudioProcessingStats> for BridgeAudioProcessingStats {
|
|||||||
/// On non-Android targets or before a voice session opens the
|
/// On non-Android targets or before a voice session opens the
|
||||||
/// section is omitted. Per SDD-090 every field in that section is
|
/// section is omitted. Per SDD-090 every field in that section is
|
||||||
/// a device-side technical scalar — no PII admitted.
|
/// a device-side technical scalar — no PII admitted.
|
||||||
#[frb(sync)]
|
pub async fn export_diagnostics() -> String {
|
||||||
pub fn export_diagnostics() -> String {
|
|
||||||
let metadata = vec![
|
let metadata = vec![
|
||||||
(
|
(
|
||||||
"crate_version".to_string(),
|
"crate_version".to_string(),
|
||||||
@@ -1247,12 +1245,15 @@ pub fn export_diagnostics() -> String {
|
|||||||
// diagnostics snapshot from the process-global slot published
|
// diagnostics snapshot from the process-global slot published
|
||||||
// by AndroidVoiceUnit::open(). Returns None on non-Android and
|
// by AndroidVoiceUnit::open(). Returns None on non-Android and
|
||||||
// before any voice session has opened.
|
// before any voice session has opened.
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
let android_audio_yaml =
|
let android_audio_yaml =
|
||||||
chanora_audio::mobile_voice_backend::current_android_audio_diagnostics()
|
chanora_audio::mobile_voice_backend::current_android_audio_diagnostics()
|
||||||
.map(|d| d.to_yaml_fragment());
|
.map(|d| d.to_yaml_fragment());
|
||||||
let audio_health = runtime().block_on(async { session().audio_processing_stats().await.ok() });
|
#[cfg(not(target_os = "android"))]
|
||||||
let network_info = runtime().block_on(async { session().network_diagnostics_summary().await });
|
let android_audio_yaml: Option<String> = None;
|
||||||
let protocol_events = runtime().block_on(async { session().drain_protocol_events().await });
|
let audio_health = session().audio_processing_stats().await.ok();
|
||||||
|
let network_info = session().network_diagnostics_summary().await;
|
||||||
|
let protocol_events = session().drain_protocol_events().await;
|
||||||
let android_audio_yaml = android_audio_yaml.map(|mut yaml| {
|
let android_audio_yaml = android_audio_yaml.map(|mut yaml| {
|
||||||
if let Some(stats) = audio_health {
|
if let Some(stats) = audio_health {
|
||||||
yaml.push_str(&format!(
|
yaml.push_str(&format!(
|
||||||
|
|||||||
@@ -370,15 +370,16 @@ fn wire__crate__api__events_stream_impl(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
fn wire__crate__api__export_diagnostics_impl(
|
fn wire__crate__api__export_diagnostics_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
rust_vec_len_: i32,
|
rust_vec_len_: i32,
|
||||||
data_len_: i32,
|
data_len_: i32,
|
||||||
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
) {
|
||||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||||
flutter_rust_bridge::for_generated::TaskInfo {
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
debug_name: "export_diagnostics",
|
debug_name: "export_diagnostics",
|
||||||
port: None,
|
port: Some(port_),
|
||||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
},
|
},
|
||||||
move || {
|
move || {
|
||||||
let message = unsafe {
|
let message = unsafe {
|
||||||
@@ -391,10 +392,15 @@ fn wire__crate__api__export_diagnostics_impl(
|
|||||||
let mut deserializer =
|
let mut deserializer =
|
||||||
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
deserializer.end();
|
deserializer.end();
|
||||||
transform_result_sse::<_, ()>((move || {
|
move |context| async move {
|
||||||
let output_ok = Result::<_, ()>::Ok(crate::api::export_diagnostics())?;
|
transform_result_sse::<_, crate::BridgeError>(
|
||||||
Ok(output_ok)
|
(move || async move {
|
||||||
})())
|
let output_ok = crate::api::export_diagnostics().await;
|
||||||
|
Ok(output_ok)
|
||||||
|
})()
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2509,6 +2515,7 @@ fn pde_ffi_dispatcher_primary_impl(
|
|||||||
7 => wire__crate__api__disconnect_impl(port, ptr, rust_vec_len, data_len),
|
7 => wire__crate__api__disconnect_impl(port, ptr, rust_vec_len, data_len),
|
||||||
8 => wire__crate__api__enable_audio_debug_wav_dump_impl(port, ptr, rust_vec_len, data_len),
|
8 => wire__crate__api__enable_audio_debug_wav_dump_impl(port, ptr, rust_vec_len, data_len),
|
||||||
9 => wire__crate__api__events_stream_impl(port, ptr, rust_vec_len, data_len),
|
9 => wire__crate__api__events_stream_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
10 => wire__crate__api__export_diagnostics_impl(port, ptr, rust_vec_len, data_len),
|
||||||
11 => wire__crate__api__get_audio_processing_config_impl(port, ptr, rust_vec_len, data_len),
|
11 => wire__crate__api__get_audio_processing_config_impl(port, ptr, rust_vec_len, data_len),
|
||||||
12 => wire__crate__api__get_ptt_binding_impl(port, ptr, rust_vec_len, data_len),
|
12 => wire__crate__api__get_ptt_binding_impl(port, ptr, rust_vec_len, data_len),
|
||||||
13 => wire__crate__api__get_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
|
13 => wire__crate__api__get_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
|
||||||
@@ -2552,7 +2559,6 @@ fn pde_ffi_dispatcher_sync_impl(
|
|||||||
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
||||||
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
||||||
match func_id {
|
match func_id {
|
||||||
10 => wire__crate__api__export_diagnostics_impl(ptr, rust_vec_len, data_len),
|
|
||||||
15 => wire__crate__api__handle_interruption_began_impl(ptr, rust_vec_len, data_len),
|
15 => wire__crate__api__handle_interruption_began_impl(ptr, rust_vec_len, data_len),
|
||||||
16 => wire__crate__api__handle_interruption_ended_impl(ptr, rust_vec_len, data_len),
|
16 => wire__crate__api__handle_interruption_ended_impl(ptr, rust_vec_len, data_len),
|
||||||
17 => wire__crate__api__handle_media_services_reset_with_route_impl(
|
17 => wire__crate__api__handle_media_services_reset_with_route_impl(
|
||||||
|
|||||||
Reference in New Issue
Block a user