feat: multi-platform bug fixes, Android audio path, and build tooling
Flutter UI fixes: - Fix stale channel badge/speaker when moved by others (derive current channel from ownClientId instead of optimistic local state) - Fix Linux PTT via focused fallback key handler - Distinguish ServerQuery clients with terminal icon in client list - Reduce duplicate current-channel badge display - Prevent PTT key-bind save from permanently closing voice settings - Fix Linux GTK reopen-after-close (quit app on window destroy) - Fix focused PTT: consume key events, release held keys on disconnect/leave-channel/mode/backend changes, suppress stale errors Flutter Rust bridge: - Thread is_server_query flag through protocol→bridge→Dart - Add own_client_id to BridgeSnapshot DTO - Add log_file_path_str() for platform log path queries Rust protocol: - Add ServerQuery test coverage (query_client_type_maps_to_server_query_flag) - Split reqwest TLS: native-tls for desktop/iOS, rustls for Android Rust audio: - Upgrade cpal 0.16→0.17.3 with API adjustments (SampleRate, description()) - Suppress Android-only dead-code warnings (open_log_file, keyring_account) Android build tooling: - tools/build-opus-android.sh: NDK auto-discovery, correct CMake Android variables (ANDROID_ABI, ANDROID_PLATFORM), portable baseline - tools/build-android-rust.sh: build+copy Rust cdylib for arm64-v8a, armeabi-v7a, x86_64 into android/app/src/main/jniLibs/ - Add jniLibs/ to .gitignore Rust bridge: - Guard open_log_file() on non-Android (Android uses logcat)
This commit is contained in:
@@ -15,7 +15,7 @@ thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
# Cross-platform audio I/O (DEC-011.1).
|
||||
cpal = "0.16"
|
||||
cpal = "0.17.3"
|
||||
# Opus encoder. tsclientlib already pulls this; we depend explicitly so
|
||||
# this crate can compile against it without going through tsclientlib.
|
||||
audiopus = "0.3.0-rc.0"
|
||||
@@ -24,19 +24,13 @@ audiopus = "0.3.0-rc.0"
|
||||
# feature. We import the crate just for the AudioHandler type; the
|
||||
# Connection type stays inside chanora_protocol.
|
||||
tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", default-features = false, features = ["audio"] }
|
||||
# Force the reqwest TLS backend to native-tls (Security.framework on
|
||||
# Apple, SChannel on Windows, system OpenSSL on Linux/BSD) instead of
|
||||
# the rustls + aws-lc-rs combination tsclientlib's `default-tls`
|
||||
# feature would otherwise pick. aws-lc-sys does not cross-compile
|
||||
# cleanly to aarch64-apple-ios, and native-tls is the standard
|
||||
# answer for "TLS that just works on every desktop+mobile platform
|
||||
# without a vendored C dependency". Cargo's feature unification means
|
||||
# this single direct dep applies to the transitive
|
||||
# tsclientlib -> reqwest chain too.
|
||||
reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "native-tls"] }
|
||||
|
||||
tokio = { version = "1", features = ["sync", "rt", "macros", "time"] }
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
# Desktop/iOS: native TLS maps to the platform TLS backend (Security.framework
|
||||
# on Apple, SChannel on Windows, system OpenSSL on Linux/BSD).
|
||||
reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "native-tls"] }
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
# Direct CoreAudio AudioUnit access on iOS (DEC-011.x follow-up).
|
||||
# cpal's iOS backend is unsuitable for VoIP: it opens
|
||||
@@ -59,6 +53,9 @@ tokio = { version = "1", features = ["sync", "rt", "macros", "time"] }
|
||||
coreaudio-rs = "0.14"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
# Android cross-builds should not pull OpenSSL. Use rustls here while keeping
|
||||
# native-tls for Apple targets where aws-lc/rustls is problematic for iOS.
|
||||
reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "rustls"] }
|
||||
# JNI bindings to flip Android's AudioManager into MODE_IN_COMMUNICATION
|
||||
# when the voice-comm preset is requested. ndk_context is initialised
|
||||
# by the bridge crate's android_init shim.
|
||||
|
||||
@@ -251,8 +251,8 @@ impl AudioEngine {
|
||||
|
||||
info!(
|
||||
target: "chanora_audio",
|
||||
in_device = %in_dev.name().unwrap_or_default(),
|
||||
out_device = %out_dev.name().unwrap_or_default(),
|
||||
in_device = %in_dev.description().map(|d| d.name().to_owned()).unwrap_or_default(),
|
||||
out_device = %out_dev.description().map(|d| d.name().to_owned()).unwrap_or_default(),
|
||||
"starting audio engine"
|
||||
);
|
||||
|
||||
@@ -270,7 +270,7 @@ impl AudioEngine {
|
||||
Ok(c) => info!(
|
||||
target: "chanora_audio",
|
||||
channels = c.channels(),
|
||||
sample_rate = c.sample_rate().0,
|
||||
sample_rate = c.sample_rate(),
|
||||
sample_format = ?c.sample_format(),
|
||||
"default_input_config reported"
|
||||
),
|
||||
@@ -284,7 +284,7 @@ impl AudioEngine {
|
||||
Ok(c) => info!(
|
||||
target: "chanora_audio",
|
||||
channels = c.channels(),
|
||||
sample_rate = c.sample_rate().0,
|
||||
sample_rate = c.sample_rate(),
|
||||
sample_format = ?c.sample_format(),
|
||||
"default_output_config reported"
|
||||
),
|
||||
@@ -409,7 +409,7 @@ impl AudioEngine {
|
||||
.default_output_config()
|
||||
.map_err(|e| AudioError::StreamConfig(format!("output default: {e}")))?;
|
||||
let out_format = out_cfg.sample_format();
|
||||
let dev_sample_rate = out_cfg.sample_rate().0;
|
||||
let dev_sample_rate = out_cfg.sample_rate();
|
||||
let dev_channels = out_cfg.channels() as usize;
|
||||
// Buffer-size rationale:
|
||||
// * Windows (WASAPI via cpal): the default period is
|
||||
@@ -840,7 +840,7 @@ fn try_open_capture(
|
||||
let in_cfg = in_dev
|
||||
.default_input_config()
|
||||
.map_err(|e| AudioError::StreamConfig(format!("input default: {e}")))?;
|
||||
let in_sample_rate = in_cfg.sample_rate().0;
|
||||
let in_sample_rate = in_cfg.sample_rate();
|
||||
let in_channels = in_cfg.channels() as usize;
|
||||
let in_format = in_cfg.sample_format();
|
||||
// Buffer-size rationale (same shape as the output path):
|
||||
@@ -1339,7 +1339,7 @@ where
|
||||
target: "chanora_audio",
|
||||
error = %e,
|
||||
requested_channels = config.channels,
|
||||
requested_sample_rate = config.sample_rate.0,
|
||||
requested_sample_rate = config.sample_rate,
|
||||
"build_output_stream FAILED"
|
||||
);
|
||||
AudioError::Backend(format!("build_output_stream: {e}"))
|
||||
|
||||
@@ -217,6 +217,7 @@ fn log_file_path() -> Option<std::path::PathBuf> {
|
||||
/// Open the platform log file in append mode, rotating once on
|
||||
/// startup so each launch begins with a fresh file. Best-effort:
|
||||
/// returns `None` on any I/O error.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn open_log_file() -> Option<std::fs::File> {
|
||||
let path = log_file_path()?;
|
||||
if let Some(parent) = path.parent() {
|
||||
@@ -278,6 +279,8 @@ pub struct BridgeClient {
|
||||
pub channel: u64,
|
||||
/// Nickname.
|
||||
pub name: String,
|
||||
/// True for TeamSpeak ServerQuery clients.
|
||||
pub is_server_query: bool,
|
||||
}
|
||||
|
||||
/// Server snapshot as seen by Dart.
|
||||
@@ -325,6 +328,7 @@ impl From<chanora_protocol::ServerSnapshot> for BridgeSnapshot {
|
||||
id: c.id.0,
|
||||
channel: c.channel.0,
|
||||
name: c.name,
|
||||
is_server_query: c.is_server_query,
|
||||
})
|
||||
.collect(),
|
||||
own_client_id: s.own_client_id,
|
||||
|
||||
@@ -1290,10 +1290,12 @@ impl SseDecode for crate::api::BridgeClient {
|
||||
let mut var_id = <u64>::sse_decode(deserializer);
|
||||
let mut var_channel = <u64>::sse_decode(deserializer);
|
||||
let mut var_name = <String>::sse_decode(deserializer);
|
||||
let mut var_isServerQuery = <bool>::sse_decode(deserializer);
|
||||
return crate::api::BridgeClient {
|
||||
id: var_id,
|
||||
channel: var_channel,
|
||||
name: var_name,
|
||||
is_server_query: var_isServerQuery,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1721,6 +1723,7 @@ impl flutter_rust_bridge::IntoDart for crate::api::BridgeClient {
|
||||
self.id.into_into_dart().into_dart(),
|
||||
self.channel.into_into_dart().into_dart(),
|
||||
self.name.into_into_dart().into_dart(),
|
||||
self.is_server_query.into_into_dart().into_dart(),
|
||||
]
|
||||
.into_dart()
|
||||
}
|
||||
@@ -1999,6 +2002,7 @@ impl SseEncode for crate::api::BridgeClient {
|
||||
<u64>::sse_encode(self.id, serializer);
|
||||
<u64>::sse_encode(self.channel, serializer);
|
||||
<String>::sse_encode(self.name, serializer);
|
||||
<bool>::sse_encode(self.is_server_query, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,23 +19,23 @@ once_cell = "1"
|
||||
# pulls in `audiopus` only — `sdl2` is a dev-dep used by upstream
|
||||
# examples; the library itself does not link SDL.
|
||||
tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", default-features = false, features = ["audio"] }
|
||||
# Force the reqwest TLS backend to native-tls (Security.framework on
|
||||
# Apple, SChannel on Windows, system OpenSSL on Linux/BSD) instead of
|
||||
# the rustls + aws-lc-rs combination tsclientlib's `default-tls`
|
||||
# feature would otherwise pick. aws-lc-sys does not cross-compile
|
||||
# cleanly to aarch64-apple-ios, and native-tls is the standard
|
||||
# answer for "TLS that just works on every desktop+mobile platform
|
||||
# without a vendored C dependency". Cargo's feature unification means
|
||||
# this single direct dep applies to the transitive
|
||||
# tsclientlib -> reqwest chain too.
|
||||
reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "native-tls"] }
|
||||
|
||||
# tsproto_packets exposes OutAudio / InAudioBuf / AudioData /
|
||||
# CodecType / Direction. Pinning to the same git rev as tsclientlib
|
||||
# avoids any version-skew confusion.
|
||||
tsproto-packets = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491" }
|
||||
tsproto-types = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491" }
|
||||
|
||||
# Async runtime utilities used by the connection task.
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "sync"] }
|
||||
futures = "0.3"
|
||||
async-trait = "0.1"
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
# Desktop/iOS: native TLS maps to the platform TLS backend (Security.framework
|
||||
# on Apple, SChannel on Windows, system OpenSSL on Linux/BSD).
|
||||
reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "native-tls"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
# Android cross-builds should not pull OpenSSL. Use rustls here while keeping
|
||||
# native-tls for Apple targets where aws-lc/rustls is problematic for iOS.
|
||||
reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "rustls"] }
|
||||
|
||||
@@ -31,6 +31,7 @@ use tsclientlib::{
|
||||
OutCommandExt, StreamItem, Version,
|
||||
};
|
||||
use tsproto_packets::packets::{InAudioBuf, OutPacket};
|
||||
use tsproto_types::ClientType;
|
||||
|
||||
use crate::dto::{ChannelId, ChannelInfo, ClientId, ClientInfo, ServerSnapshot};
|
||||
use crate::ProtocolError;
|
||||
@@ -872,6 +873,7 @@ fn build_snapshot(con: &Connection) -> Result<ServerSnapshot, ProtocolError> {
|
||||
id: ClientId(c.id.0 as u64),
|
||||
channel: ChannelId(c.channel.0),
|
||||
name: sanitize(&c.name),
|
||||
is_server_query: is_server_query_client_type(&c.client_type),
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -886,6 +888,10 @@ fn build_snapshot(con: &Connection) -> Result<ServerSnapshot, ProtocolError> {
|
||||
})
|
||||
}
|
||||
|
||||
fn is_server_query_client_type(client_type: &ClientType) -> bool {
|
||||
matches!(client_type, ClientType::Query { .. })
|
||||
}
|
||||
|
||||
/// Light sanitisation of strings before they cross the protocol
|
||||
/// boundary. The redaction policy proper lives in
|
||||
/// `chanora_diagnostics`; this filter only strips control characters
|
||||
@@ -905,7 +911,8 @@ const _ROOT_MATCHES_UPSTREAM: () = {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::sort_channels_tree_by;
|
||||
use super::{is_server_query_client_type, sort_channels_tree_by};
|
||||
use tsproto_types::ClientType;
|
||||
|
||||
/// Lightweight fixture mirroring just the (id, parent, order)
|
||||
/// triple that the linked-list sort needs. Avoids constructing
|
||||
@@ -922,6 +929,13 @@ mod tests {
|
||||
(c.id, c.parent, c.order)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_client_type_maps_to_server_query_flag() {
|
||||
assert!(!is_server_query_client_type(&ClientType::Normal));
|
||||
assert!(is_server_query_client_type(&ClientType::Query { admin: false }));
|
||||
assert!(is_server_query_client_type(&ClientType::Query { admin: true }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_sort_linked_list_under_one_parent() {
|
||||
// Server emits four root-level channels in arbitrary HashMap
|
||||
|
||||
@@ -34,6 +34,8 @@ pub struct ClientInfo {
|
||||
pub channel: ChannelId,
|
||||
/// Nickname, preserved verbatim per ADR-008.
|
||||
pub name: String,
|
||||
/// True for TeamSpeak ServerQuery clients.
|
||||
pub is_server_query: bool,
|
||||
}
|
||||
|
||||
/// Snapshot of the server's published state at a moment in time.
|
||||
|
||||
@@ -173,6 +173,7 @@ pub struct IdentityFileStore {
|
||||
/// Stable per-install identifier used as the keyring account
|
||||
/// name. Derived from the install directory so the same store
|
||||
/// finds the same keyring entry across restarts.
|
||||
#[cfg_attr(target_os = "android", allow(dead_code))]
|
||||
keyring_account: String,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user