22 Commits
Author SHA1 Message Date
Del Wang 5d187f5d5b docs(v2): 开发中,无限期 delay 2026-01-08 09:55:30 +08:00
Del Wang 3cf6771cc9 refactor: 异步 RPC 2026-01-08 09:53:59 +08:00
Del Wang fb6c1fd9f8 refactor: 事件收发与处理 2026-01-06 18:57:14 +08:00
Del Wang 487a5b8c14 refactor: 优化推流播放与录音,支持立体声 2026-01-06 14:06:15 +08:00
Del Wang 0125a1478e refactor: 优化音频读取与编码传输 2026-01-06 12:08:06 +08:00
Del Wang c0bc4b8ae6 fix: 修复服务端推流播放异常的问题 2026-01-05 20:13:25 +08:00
Del Wang d88d63b15b chore: polish code 2026-01-04 15:06:55 +08:00
Del Wang cce3dd630a refactor: 重构 Open-XiaoAI Client V2 2026-01-04 11:24:37 +08:00
Del Wang 052ce06355 chore: polish client/server code 2026-01-04 10:42:06 +08:00
Del Wang eeb442d5b8 refactor: 分离音频管理模块 2026-01-03 19:26:35 +08:00
Del Wang 34924fa06e chore: refine client/server mod 2026-01-03 19:17:13 +08:00
Del Wang c4f7fbec6d chore: test client call server 2026-01-02 17:37:27 +08:00
Del Wang a2d06958e1 chore: 测试播放和录音 2026-01-02 17:23:24 +08:00
Del Wang 24b3fdddf8 fix(stereo): DISCOVERY_MAGIC 2026-01-02 16:25:48 +08:00
Del Wang 6f55021651 refactor: handlers 2026-01-02 15:16:31 +08:00
Del Wang 4ed8d3dca9 chore: 初始化测试场景 2026-01-02 13:04:19 +08:00
Del Wang 33da56dead fix: 修复 runtime 镜像 2026-01-02 12:50:23 +08:00
Del Wang ef28121f5d refactor: 调整模块位置 2026-01-02 10:22:16 +08:00
Del Wang 4d459afd28 chore: 初始化 client v2 2026-01-02 09:59:29 +08:00
Del Wang aa2a5d8474 chore(stereo): 优化播放设备重初始化 2026-01-01 20:27:42 +08:00
Del Wang b69bd5a8f9 fix(stereo): 修复主从节点同步异常的问题 2026-01-01 19:58:04 +08:00
Del Wang f4340f068f chore: 更新固件版本 2026-01-01 17:12:02 +08:00
43 changed files with 5476 additions and 209 deletions
+6 -2
View File
@@ -91,7 +91,8 @@ pub async fn run_master(master_role: ChannelRole) -> Result<()> {
let mut opus_out = vec![0u8; 1500];
let mut seq = 0u32;
let delay_us = 200_000;
// 播放延迟: 只需覆盖网络延迟 + 时钟偏移
let delay_us = 100_000; // 100ms 基础延迟
let frame_duration_us =
(config.frame_size as f64 / config.sample_rate as f64 * 1_000_000.0) as u128;
@@ -142,7 +143,8 @@ pub async fn run_master(master_role: ChannelRole) -> Result<()> {
stream_start_seq = seq;
}
// 计算该帧应当播放的基准时间(相对于流开始)
// 计算该帧应当播放的目标时间
// target_ts = 数据包发送时间 + 播放延迟
let target_ts = stream_start_ts
+ ((seq - stream_start_seq) as u128 * frame_duration_us)
+ delay_us;
@@ -215,6 +217,8 @@ pub async fn run_master(master_role: ChannelRole) -> Result<()> {
let wait = target_ts - now;
if wait > 1000 {
tokio::time::sleep(Duration::from_micros(wait as u64)).await;
} else {
// 小于 1ms,直接播放,让播放时机稍微早一点点
}
}
} else {
+36 -38
View File
@@ -6,7 +6,6 @@ use crate::audio::player::AudioPlayer;
use crate::net::discovery::Discovery;
use crate::net::network::SlaveNetwork;
use crate::net::protocol::{AudioPacket, ChannelRole, ControlPacket};
use crate::utils::jitter_buffer::JitterBuffer;
use crate::utils::sync::{ClockSync, now_us};
use anyhow::{Result, anyhow};
use std::sync::Arc;
@@ -62,7 +61,6 @@ async fn handle_connection(role: ChannelRole) -> Result<()> {
};
let player = AudioPlayer::new(&config)?;
let mut codec = OpusCodec::new(&config)?;
let mut jitter = JitterBuffer::new(50_000, 3);
let clock = Arc::new(Mutex::new(ClockSync::new(100)));
// 用于通知主循环 TCP 已断开的消息通道
@@ -85,7 +83,7 @@ async fn handle_connection(role: ChannelRole) -> Result<()> {
let _ = d_tx_ping.send(()).await; // 通知主线程 TCP 失败
break;
}
tokio::time::sleep(Duration::from_secs(1)).await;
tokio::time::sleep(Duration::from_millis(200)).await;
seq += 1;
}
});
@@ -130,7 +128,7 @@ async fn handle_connection(role: ChannelRole) -> Result<()> {
}
});
// 8. 播放主循环
// 8. 播放提示
println!("✅ 主节点已连接,音频串流中...");
let role_str = role.to_string();
tokio::spawn(async move {
@@ -144,9 +142,9 @@ async fn handle_connection(role: ChannelRole) -> Result<()> {
.await;
});
// 9. 播放主循环
let mut pcm_buf = vec![0i16; config.frame_size];
let mut last_seq: Option<u32> = None;
let mut last_packet_time = now_us();
loop {
// 检查 TCP 是否已断开
@@ -154,48 +152,48 @@ async fn handle_connection(role: ChannelRole) -> Result<()> {
return Err(anyhow!("主节点已断开: {}", master_tcp_addr));
}
// 填充 Jitter Buffer
while let Ok(pkt) = audio_rx.try_recv() {
// 接收数据包
if let Ok(pkt) = audio_rx.try_recv() {
let now = now_us();
// 如果超过 500ms 没有收到包,认为是新流开始,重置状态
if now - last_packet_time > 500_000 {
jitter.clear();
last_seq = None;
codec = OpusCodec::new(&config)?;
let _ = player.prepare();
let current_server_time = clock.lock().await.to_server_time(now);
// 检查包是否迟到(目标时间已过)
if current_server_time > pkt.timestamp {
let late_ms = (current_server_time - pkt.timestamp) / 1000;
if late_ms > 50 {
// 迟到超过 50ms,直接丢弃
continue;
}
// 轻微迟到(<50ms),尝试播放
}
last_packet_time = now;
jitter.push(pkt);
}
let now = now_us();
let current_server_time = clock.lock().await.to_server_time(now);
last_seq = Some(pkt.seq);
if let Some((seq, data)) = jitter.pop_frame(current_server_time) {
if let Some(last) = last_seq {
let loss_count = seq.wrapping_sub(last) as i32 - 1;
if loss_count > 0 {
// 1. 优先尝试 FEC 恢复最近丢失的那一帧
// Opus 的 FEC 数据存储在当前包(data)中,用于恢复“前一帧”
if let Ok(len) = codec.decode_fec(&data, &mut pcm_buf) {
let _ = player.write(&pcm_buf[..len]);
}
// 精确等待到播放时间
loop {
let now = now_us();
let current_server_time_precise = clock.lock().await.to_server_time(now);
// 2. 如果丢包超过 1 帧,剩下的帧只能靠丢包补偿(PLC)
for _ in 0..(loss_count - 1) {
if let Ok(len) = codec.decode_loss(&mut pcm_buf) {
let _ = player.write(&pcm_buf[..len]);
}
}
if current_server_time_precise >= pkt.timestamp {
break;
}
let wait_us = (pkt.timestamp - current_server_time_precise) as u64;
if wait_us > 1000 {
tokio::time::sleep(Duration::from_micros(wait_us as u64)).await;
} else {
// 小于 1ms,直接播放,让播放时机稍微早一点点
break;
}
}
last_seq = Some(seq);
// 3. 正常解码当前帧
let len = codec.decode(&data, &mut pcm_buf)?;
player.write(&pcm_buf[..len])?;
// 解码并播放
if let Ok(len) = codec.decode(&pkt.data, &mut pcm_buf) {
let _ = player.write(&pcm_buf[..len]);
}
} else {
tokio::time::sleep(Duration::from_millis(10)).await;
tokio::time::sleep(Duration::from_micros(100)).await;
}
}
}
+9 -8
View File
@@ -52,18 +52,19 @@ fn setup_pcm(pcm: &PCM, sample_rate: u32, channels: u16) -> Result<()> {
hwp.set_rate(sample_rate, alsa::ValueOr::Nearest)?;
hwp.set_channels(channels as u32)?;
// // 设置较大的缓冲区以减少由于调度抖动导致的断音
// // 48000Hz * 0.2s = 9600 samples
// let buffer_size = (sample_rate as f64 * 0.2) as u32;
// let period_size = buffer_size / 4;
// hwp.set_buffer_size_near(buffer_size as alsa::pcm::Frames)?;
// hwp.set_period_size_near(period_size as alsa::pcm::Frames, alsa::ValueOr::Nearest)?;
// 设置较大的缓冲区以减少由于调度抖动和设备重初始化导致的断音/卡顿
// 使用 100ms 缓冲区,既能防止 underrun,又不会引入过大延迟
let buffer_size = (sample_rate as f64 * 0.1) as u32; // 100ms 缓冲
let period_size = buffer_size / 4; // 25ms 周期
hwp.set_buffer_size_near(buffer_size as alsa::pcm::Frames)?;
hwp.set_period_size_near(period_size as alsa::pcm::Frames, alsa::ValueOr::Nearest)?;
pcm.hw_params(&hwp).context("Failed to set HwParams")?;
let swp = pcm.sw_params_current()?;
// 设置 start_threshold 为 buffer_size 的一半,确保缓冲区有足够数据再开始播放
// swp.set_start_threshold(buffer_size as alsa::pcm::Frames / 2)?;
// 设置 start_threshold,当缓冲区有 1 个 period 数据时就开始播放
// 这样可以快速启动,同时保持足够的缓冲余量
swp.set_start_threshold(period_size as alsa::pcm::Frames)?;
pcm.sw_params(&swp)?;
pcm.prepare()?;
Ok(())
+10 -5
View File
@@ -5,6 +5,7 @@ use std::time::Duration;
use tokio::net::UdpSocket;
pub const DISCOVERY_PORT: u16 = 53530;
const DISCOVERY_MAGIC: &[u8] = b"STEREO_DISCOVERY_V1";
/// 服务发现模块,用于主从节点的自动发现
pub struct Discovery;
@@ -16,7 +17,9 @@ impl Discovery {
socket.set_broadcast(true)?;
let target: SocketAddr = format!("255.255.255.255:{}", DISCOVERY_PORT).parse()?;
let msg = postcard::to_allocvec(&ControlPacket::ServerHello { udp_port: tcp_port })?;
let mut msg = DISCOVERY_MAGIC.to_vec();
msg.extend(postcard::to_allocvec(&ControlPacket::ServerHello { udp_port: tcp_port })?);
tokio::spawn(async move {
loop {
@@ -35,10 +38,12 @@ impl Discovery {
loop {
let (len, addr) = socket.recv_from(&mut buf).await?;
if let Ok(ControlPacket::ServerHello { udp_port }) =
postcard::from_bytes::<ControlPacket>(&buf[..len])
{
return Ok((addr.ip(), udp_port));
let data = &buf[..len];
if data.starts_with(DISCOVERY_MAGIC) {
let packet_data = &data[DISCOVERY_MAGIC.len()..];
if let Ok(ControlPacket::ServerHello { udp_port }) = postcard::from_bytes(packet_data) {
return Ok((addr.ip(), udp_port));
}
}
}
}
-100
View File
@@ -1,100 +0,0 @@
use std::cmp::Ordering;
use std::cmp::Reverse;
use std::collections::BinaryHeap;
use crate::net::protocol::AudioPacket;
#[derive(Debug)]
struct OrderedPacket {
seq: u32,
timestamp: u128,
data: Vec<u8>,
}
// 仅针对序列号进行比较,处理回绕逻辑
impl PartialEq for OrderedPacket {
fn eq(&self, other: &Self) -> bool {
self.seq == other.seq
}
}
impl Eq for OrderedPacket {}
impl PartialOrd for OrderedPacket {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for OrderedPacket {
fn cmp(&self, other: &Self) -> Ordering {
// 使用 wrapping_sub 处理 u32 回绕 (Rollover)
let diff = self.seq.wrapping_sub(other.seq) as i32;
diff.cmp(&0)
}
}
pub struct JitterBuffer {
// 使用 Reverse 将 BinaryHeap 变为小顶堆,避免手动实现大量 Trait
buffer: BinaryHeap<Reverse<OrderedPacket>>,
last_played_seq: Option<u32>,
pub target_delay_us: u128,
min_packets: usize, // 最小缓冲包数,防止微小抖动
}
impl JitterBuffer {
pub fn new(target_delay_us: u128, min_packets: usize) -> Self {
Self {
buffer: BinaryHeap::with_capacity(32), // 预分配初始容量
last_played_seq: None,
target_delay_us,
min_packets,
}
}
pub fn push(&mut self, packet: AudioPacket) {
// 1. 处理序列号回绕的丢包逻辑
if let Some(last) = self.last_played_seq {
let diff = packet.seq.wrapping_sub(last) as i32;
if diff <= 0 {
return; // 这是一个延迟到达的旧包,直接丢弃
}
}
self.buffer.push(Reverse(OrderedPacket {
seq: packet.seq,
timestamp: packet.timestamp,
data: packet.data,
}));
}
pub fn pop_frame(&mut self, current_time: u128) -> Option<(u32, Vec<u8>)> {
// 2. 预缓冲逻辑:如果包量太少,先不播放,等待填充
if self.buffer.len() < self.min_packets && self.last_played_seq.is_none() {
return None;
}
// 3. 检查堆顶元素
if let Some(Reverse(pkt)) = self.buffer.peek() {
// 判断是否到达播放时间(考虑目标延迟)
if current_time >= pkt.timestamp + (self.target_delay_us / 1000) {
let Reverse(pkt) = self.buffer.pop().unwrap();
self.last_played_seq = Some(pkt.seq);
return Some((pkt.seq, pkt.data));
}
}
None
}
/// 如果缓冲区堆积过大,可以主动跳帧以降低延迟
pub fn shrink_to_fit_latency(&mut self, max_size: usize) {
while self.buffer.len() > max_size {
self.buffer.pop();
}
}
pub fn clear(&mut self) {
self.buffer.clear();
self.last_played_seq = None;
}
}
-1
View File
@@ -1,3 +1,2 @@
pub mod alsa;
pub mod jitter_buffer;
pub mod sync;
+177 -16
View File
@@ -10,10 +10,38 @@ pub fn now_us() -> u128 {
}
/// 时钟同步管理器,用于计算主从节点间的时钟偏移
/// 采用改进的 NTP 算法 + Kalman 滤波思想
pub struct ClockSync {
offsets: VecDeque<i128>,
/// 偏移量样本窗口
offsets: VecDeque<OffsetSample>,
/// 当前估计的时钟偏移 (server_time - client_time)
pub current_offset: i128,
/// RTT 样本窗口
rtts: VecDeque<i128>,
/// 当前估计的最小 RTT
min_rtt: i128,
/// 窗口大小
window_size: usize,
/// 时钟漂移率 (ppm: parts per million)
/// 正值表示从节点时钟比主节点快
drift_rate: f64,
/// 上次更新时间
last_update_time: u128,
/// 漂移率估计窗口
drift_samples: VecDeque<DriftSample>,
}
#[derive(Clone, Copy)]
struct OffsetSample {
offset: i128,
rtt: i128,
timestamp: u128,
}
#[derive(Clone, Copy)]
struct DriftSample {
offset: i128,
timestamp: u128,
}
impl ClockSync {
@@ -21,43 +49,176 @@ impl ClockSync {
Self {
offsets: VecDeque::with_capacity(window_size),
current_offset: 0,
rtts: VecDeque::with_capacity(window_size),
min_rtt: i128::MAX,
window_size,
drift_rate: 0.0,
last_update_time: now_us(),
drift_samples: VecDeque::with_capacity(60), // 保留 60 秒的样本
}
}
/// 更新时钟偏移估计
/// 更新时钟偏移估计 (NTP 算法)
///
/// NTP 时间戳标记:
/// t1 = client_send_ts : 客户端发送 Ping 的时间
/// t2 = server_ts : 服务器接收 Ping 的时间
/// t3 = server_ts : 服务器发送 Pong 的时间 (假设处理时间忽略不计)
/// t4 = client_recv_ts : 客户端接收 Pong 的时间
///
/// RTT = (t4 - t1) - (t3 - t2) = (t4 - t1) (因为 t3 = t2)
/// Offset = ((t2 - t1) + (t3 - t4)) / 2 = ((t2 - t1) + (t2 - t4)) / 2
/// = t2 - (t1 + t4) / 2
pub fn update(&mut self, client_send_ts: u128, server_ts: u128, client_recv_ts: u128) {
let rtt = (client_recv_ts - client_send_ts) as i128;
// 基础过滤:如果 RTT 过大则忽略 (例如局域网内 > 100ms)
if rtt > 100_000 {
let t1 = client_send_ts as i128;
let t2 = server_ts as i128;
let t4 = client_recv_ts as i128;
let rtt = t4 - t1;
// 过滤异常 RTT (局域网内 > 100ms 视为异常)
if rtt < 0 || rtt > 100_000 {
return;
}
// 时钟偏移 = 主节点时间 - 从节点时间
// 假设主节点收到 Ping 的时间点在 (发送时间 + 接收时间) / 2
let estimated_server_time = server_ts as i128 + rtt / 2;
let offset = estimated_server_time - client_recv_ts as i128;
// 计算时钟偏移: offset = server_time - client_time
// offset = t2 - (t1 + t4) / 2
let offset = t2 - (t1 + t4) / 2;
self.offsets.push_back(offset);
// 更新 RTT 窗口
self.rtts.push_back(rtt);
if self.rtts.len() > self.window_size {
self.rtts.pop_front();
}
self.min_rtt = *self.rtts.iter().min().unwrap_or(&rtt);
// 更新偏移量窗口
let sample = OffsetSample {
offset,
rtt,
timestamp: client_recv_ts,
};
self.offsets.push_back(sample);
if self.offsets.len() > self.window_size {
self.offsets.pop_front();
}
// 计算中位数偏移,以增强抗干扰能力
let mut sorted: Vec<i128> = self.offsets.iter().cloned().collect();
sorted.sort_unstable();
if !sorted.is_empty() {
self.current_offset = sorted[sorted.len() / 2];
// 偏移量估计: 使用低 RTT 样本的中位数
// 原理: RTT 较小的样本受网络抖动影响小,时间测量更准确
let mut low_rtt_offsets: Vec<i128> = self
.offsets
.iter()
.filter(|s| s.rtt <= self.min_rtt + 5000) // 5ms 容差
.map(|s| s.offset)
.collect();
if !low_rtt_offsets.is_empty() {
low_rtt_offsets.sort_unstable();
let new_offset = low_rtt_offsets[low_rtt_offsets.len() / 2];
// 漂移率估计
self.estimate_drift(new_offset, client_recv_ts);
// 平滑更新偏移量 (避免突变)
let alpha = 0.3; // 低通滤波系数
self.current_offset =
(alpha * new_offset as f64 + (1.0 - alpha) * self.current_offset as f64) as i128;
}
self.last_update_time = client_recv_ts;
}
/// 估计时钟漂移率
/// 时钟漂移率 = d(offset) / dt
fn estimate_drift(&mut self, offset: i128, timestamp: u128) {
self.drift_samples.push_back(DriftSample { offset, timestamp });
if self.drift_samples.len() > 60 {
self.drift_samples.pop_front();
}
// 至少需要 10 秒的数据才能估计漂移
if self.drift_samples.len() < 10 {
return;
}
// 使用线性回归估计漂移率
let first = self.drift_samples.front().unwrap();
let last = self.drift_samples.back().unwrap();
let dt = (last.timestamp - first.timestamp) as f64;
let d_offset = (last.offset - first.offset) as f64;
if dt > 10_000_000.0 {
// 超过 10 秒
// drift_rate 单位: 微秒/秒 = ppm
let new_drift = d_offset / (dt / 1_000_000.0);
// 平滑更新漂移率
let beta = 0.1;
self.drift_rate = beta * new_drift + (1.0 - beta) * self.drift_rate;
}
}
/// 将本地时间转换为服务器(主节点)时间
/// 考虑时钟漂移补偿
pub fn to_server_time(&self, client_time: u128) -> u128 {
(client_time as i128 + self.current_offset) as u128
let base_server_time = (client_time as i128 + self.current_offset) as u128;
// 漂移补偿: 根据距离上次同步的时间,补偿时钟漂移
let elapsed_since_update = client_time.saturating_sub(self.last_update_time) as f64;
let drift_correction = (self.drift_rate * elapsed_since_update / 1_000_000.0) as i128;
(base_server_time as i128 + drift_correction) as u128
}
/// 将服务器(主节点)时间转换为本地时间
pub fn to_client_time(&self, server_time: u128) -> u128 {
// 简化版本,不考虑漂移补偿 (播放时主要用 to_server_time)
(server_time as i128 - self.current_offset) as u128
}
/// 获取当前估计的 RTT (微秒)
pub fn get_rtt(&self) -> i128 {
self.min_rtt
}
/// 获取当前时钟漂移率 (ppm)
pub fn get_drift_rate(&self) -> f64 {
self.drift_rate
}
/// 获取同步质量评估 (0-100, 越高越好)
pub fn get_sync_quality(&self) -> u8 {
if self.offsets.is_empty() {
return 0;
}
// 基于 RTT 稳定性和偏移量方差评估
let rtt_variance = self.calculate_variance(&self.rtts.iter().copied().collect::<Vec<_>>());
let offset_variance = self.calculate_variance(
&self.offsets.iter().map(|s| s.offset).collect::<Vec<_>>(),
);
// RTT 越稳定,方差越小,质量越高
let rtt_score = ((100_000.0 - rtt_variance.min(100_000.0)) / 100_000.0 * 50.0) as u8;
let offset_score = ((50_000.0 - offset_variance.min(50_000.0)) / 50_000.0 * 50.0) as u8;
rtt_score + offset_score
}
fn calculate_variance(&self, samples: &[i128]) -> f64 {
if samples.is_empty() {
return 0.0;
}
let mean = samples.iter().sum::<i128>() as f64 / samples.len() as f64;
let variance = samples
.iter()
.map(|&x| {
let diff = x as f64 - mean;
diff * diff
})
.sum::<f64>()
/ samples.len() as f64;
variance.sqrt()
}
}
+4 -4
View File
@@ -14,8 +14,8 @@
你可以直接在 [Github Releases](https://github.com/idootop/open-xiaoai/releases) 页面下载打包好的固件:
- [Xiaomi 智能音箱 Pro v1.58.1](https://github.com/idootop/open-xiaoai/releases/tag/OH2P_1.58.1)
- [小爱音箱 Pro v1.88.221](https://github.com/idootop/open-xiaoai/releases/tag/LX06_1.88.221)
- [Xiaomi 智能音箱 Pro v1.58.6](https://github.com/idootop/open-xiaoai/releases/tag/OH2P_1.58.6)
- [小爱音箱 Pro v1.94.13](https://github.com/idootop/open-xiaoai/releases/tag/LX06_1.94.13)
> [!TIP]
> 里面有两个文件,下载 `patched` 那个:
@@ -33,8 +33,8 @@
> [!CAUTION]
> 当前支持的最新固件版本为:
>
> - Xiaomi 智能音箱 Pro 👉 [v1.58.1](https://github.com/idootop/open-xiaoai/releases/tag/OH2P_1.58.1)
> - 小爱音箱 Pro 👉 [v1.88.221](https://github.com/idootop/open-xiaoai/releases/tag/LX06_1.88.221)
> - Xiaomi 智能音箱 Pro 👉 [v1.58.6](https://github.com/idootop/open-xiaoai/releases/tag/OH2P_1.58.6)
> - 小爱音箱 Pro 👉 [v1.94.13](https://github.com/idootop/open-xiaoai/releases/tag/LX06_1.94.13)
>
> 更新版本的固件可能存在变化,导致刷机失败,设备变砖,请自行评估风险。
+2
View File
@@ -0,0 +1,2 @@
*.mp3
*.wav
+853
View File
@@ -0,0 +1,853 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "alsa"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d"
dependencies = [
"alsa-sys",
"bitflags 2.10.0",
"cfg-if",
"libc",
]
[[package]]
name = "alsa-sys"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04"
dependencies = [
"libc",
"pkg-config",
]
[[package]]
name = "anyhow"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "atomic-polyfill"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
dependencies = [
"critical-section",
]
[[package]]
name = "audiopus_sys"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62314a1546a2064e033665d658e88c620a62904be945f8147e6b16c3db9f8651"
dependencies = [
"cmake",
"log",
"pkg-config",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "bytemuck"
version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "cc"
version = "1.2.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cmake"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
dependencies = [
"cc",
]
[[package]]
name = "cobs"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
dependencies = [
"thiserror",
]
[[package]]
name = "critical-section"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "dashmap"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
dependencies = [
"cfg-if",
"crossbeam-utils",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "embedded-io"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
[[package]]
name = "embedded-io"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "extended"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
[[package]]
name = "find-msvc-tools"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
"futures-macro",
"futures-task",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "hash32"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
dependencies = [
"byteorder",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
[[package]]
name = "heapless"
version = "0.7.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
dependencies = [
"atomic-polyfill",
"hash32",
"rustc_version",
"serde",
"spin",
"stable_deref_trait",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "mio"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
"wasi",
"windows-sys 0.61.2",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "opus"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6526409b274a7e98e55ff59d96aafd38e6cd34d46b7dbbc32ce126dffcd75e8e"
dependencies = [
"audiopus_sys",
"libc",
]
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "postcard"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
dependencies = [
"cobs",
"embedded-io 0.4.0",
"embedded-io 0.6.1",
"heapless",
"serde",
]
[[package]]
name = "proc-macro2"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags 2.10.0",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "semver"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "slab"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
dependencies = [
"libc",
"windows-sys 0.60.2",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "symphonia"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039"
dependencies = [
"lazy_static",
"symphonia-bundle-flac",
"symphonia-bundle-mp3",
"symphonia-codec-adpcm",
"symphonia-codec-pcm",
"symphonia-codec-vorbis",
"symphonia-core",
"symphonia-format-mkv",
"symphonia-format-ogg",
"symphonia-format-riff",
"symphonia-metadata",
]
[[package]]
name = "symphonia-bundle-flac"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976"
dependencies = [
"log",
"symphonia-core",
"symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-bundle-mp3"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed"
dependencies = [
"lazy_static",
"log",
"symphonia-core",
"symphonia-metadata",
]
[[package]]
name = "symphonia-codec-adpcm"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dddc50e2bbea4cfe027441eece77c46b9f319748605ab8f3443350129ddd07f"
dependencies = [
"log",
"symphonia-core",
]
[[package]]
name = "symphonia-codec-pcm"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95"
dependencies = [
"log",
"symphonia-core",
]
[[package]]
name = "symphonia-codec-vorbis"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73"
dependencies = [
"log",
"symphonia-core",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-core"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af"
dependencies = [
"arrayvec",
"bitflags 1.3.2",
"bytemuck",
"lazy_static",
"log",
]
[[package]]
name = "symphonia-format-mkv"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "122d786d2c43a49beb6f397551b4a050d8229eaa54c7ddf9ee4b98899b8742d0"
dependencies = [
"lazy_static",
"log",
"symphonia-core",
"symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-format-ogg"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb"
dependencies = [
"log",
"symphonia-core",
"symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-format-riff"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f"
dependencies = [
"extended",
"log",
"symphonia-core",
"symphonia-metadata",
]
[[package]]
name = "symphonia-metadata"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16"
dependencies = [
"encoding_rs",
"lazy_static",
"log",
"symphonia-core",
]
[[package]]
name = "symphonia-utils-xiph"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16"
dependencies = [
"symphonia-core",
"symphonia-metadata",
]
[[package]]
name = "syn"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio"
version = "1.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio-util"
version = "0.7.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
dependencies = [
"bytes",
"futures-core",
"futures-io",
"futures-sink",
"futures-util",
"hashbrown 0.15.5",
"pin-project-lite",
"slab",
"tokio",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "xiao"
version = "0.1.0"
dependencies = [
"alsa",
"anyhow",
"dashmap",
"opus",
"parking_lot",
"postcard",
"serde",
"symphonia",
"thiserror",
"tokio",
"tokio-util",
]
+29
View File
@@ -0,0 +1,29 @@
[package]
name = "xiao"
version = "0.1.0"
edition = "2024"
[profile.release]
lto = true
opt-level = "s"
codegen-units = 1
panic = "abort"
strip = true
debug = false
[dependencies]
opus = "0.3"
anyhow = "1.0"
tokio = { version = "1.48", features = ["full"] }
tokio-util = { version = "0.7", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
postcard = { version = "1.0", features = ["alloc", "use-std"] }
parking_lot = "0.12.5"
dashmap = "6.1.0"
thiserror = "2.0.17"
[target.'cfg(target_os = "linux")'.dependencies]
alsa = "0.11"
[target.'cfg(not(target_os = "linux"))'.dependencies]
symphonia = { version = "0.5", features = ["mp3", "wav", "pcm"] }
+17
View File
@@ -0,0 +1,17 @@
build-server:
cargo build --release --bin server
build-client:
docker run --rm -v $(shell pwd):/app idootop/open-xiaoai-runtime:oh2p \
cargo build --target armv7-unknown-linux-gnueabihf --release --bin client
run-server:
target/release/server
test:
clear && make build-client && make deploy && make build-server && make run-server
# 部署到小爱音箱(调试自用)
deploy:
dd if=target/armv7-unknown-linux-gnueabihf/release/client \
| sshpass -p open-xiaoai ssh -o HostKeyAlgorithms=+ssh-rsa root@192.168.31.235 "dd of=/data/client"
+7
View File
@@ -0,0 +1,7 @@
# Open-XiaoAI Client V2
> 开发中,敬请期待...
## License
MIT License © 2026-PRESENT [Del Wang](https://del.wang)
+351
View File
@@ -0,0 +1,351 @@
//! # Client 模块
//!
//! 音频客户端,支持:
//! - 服务发现和自动连接
//! - 音频录制和播放
//! - RPC 远程调用
//! - 实时事件处理
//!
//! ## 架构
//!
//! ```text
//! ┌─────────────────────────────────────┐
//! │ Client │
//! │ │
//! Server ◀──TCP─────┼──▶ Session │
//! │ ├─ Connection │
//! │ ├─ RPC Manager │
//! │ └─ Active Pipelines │
//! │ │
//! Server ◀──UDP─────┼──▶ Audio Pipelines │
//! │ ├─ RecordPipeline │
//! │ └─ PlaybackPipeline │
//! │ │
//! Events ◀──────────┼──▶ Event Handlers │
//! └─────────────────────────────────────┘
//! ```
mod pipeline;
mod session;
pub use pipeline::{PipelineHandle, PlaybackPipeline, RecordPipeline};
pub use session::Session;
use crate::net::command::CommandResult;
use crate::net::discovery::Discovery;
use crate::net::event::{EventBus, EventBusSubscription, EventData};
use crate::net::network::{AudioSocket, Connection};
use crate::net::protocol::ControlPacket;
use crate::net::rpc::RpcBuilder;
use crate::net::sync::now_us;
use anyhow::{Result, anyhow};
use session::handshake;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio_util::sync::CancellationToken;
/// 客户端配置
#[derive(Debug, Clone)]
pub struct ClientConfig {
/// 版本
pub version: String,
/// 客户端认证
pub client_auth: String,
/// 服务端认证
pub server_auth: String,
/// 心跳间隔(毫秒)
pub heartbeat_ms: u64,
/// 连接超时(秒)
pub timeout: u64,
/// 客户端型号
pub model: String,
/// 序列号
pub serial_number: String,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
version: env!("CARGO_PKG_VERSION").to_string(),
server_auth: std::env::var("XIAO_SERVER_AUTH")
.unwrap_or_else(|_| "xiao-server".to_string()),
client_auth: std::env::var("XIAO_CLIENT_AUTH")
.unwrap_or_else(|_| "xiao-client".to_string()),
heartbeat_ms: 200,
timeout: 60,
// todo 获取设备信息
model: "Open-XiaoAi-V2".to_string(),
serial_number: "00:00:00:00:00:00".to_string(),
}
}
}
/// 音频客户端
pub struct Client {
/// 配置
config: ClientConfig,
/// 当前活动会话
session: RwLock<Option<Arc<Session>>>,
/// 全局取消令牌
cancel: CancellationToken,
/// 事件总线
event_bus: Arc<EventBus>,
}
impl Client {
/// 创建新客户端
pub fn new(config: ClientConfig) -> Self {
Self {
config,
session: RwLock::new(None),
cancel: CancellationToken::new(),
event_bus: Arc::new(EventBus::default()),
}
}
/// 使用默认配置创建客户端
pub fn with_defaults() -> Self {
Self::new(ClientConfig::default())
}
/// 订阅事件
pub fn subscribe_events(&self) -> EventBusSubscription {
self.event_bus.subscribe()
}
/// 运行客户端(自动发现服务器并连接)
pub async fn run(self: Arc<Self>) -> Result<()> {
loop {
tokio::select! {
_ = self.cancel.cancelled() => {
println!("[Client] Shutting down...");
break;
}
result = self.discover_and_connect() => {
if let Err(e) = result {
eprintln!("[Client] Connection error: {}", e);
}
// 连接断开后清理并重试
self.cleanup().await;
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
}
Ok(())
}
/// 发现服务器并连接
async fn discover_and_connect(&self) -> Result<()> {
println!("[Client] Searching for server...");
let (ip, tcp_port) = Discovery::listen().await?;
let server_addr = SocketAddr::new(ip, tcp_port);
println!("[Client] Found server at {}", server_addr);
let stream = tokio::net::TcpStream::connect(server_addr).await?;
println!("[Client] Connected to {}", server_addr);
self.handle_session(stream, server_addr).await
}
/// 处理会话
async fn handle_session(
&self,
stream: tokio::net::TcpStream,
server_addr: SocketAddr,
) -> Result<()> {
let conn = Arc::new(Connection::new(stream)?);
let audio_socket = Arc::new(AudioSocket::bind().await?);
// 执行握手
let handshake_result =
handshake(&conn, audio_socket.port(), server_addr, &self.config).await?;
println!(
"[Client] Handshake OK, server audio at {}",
handshake_result.server_audio_addr
);
// 创建会话
let session_cancel = self.cancel.child_token();
let session = Arc::new(Session::new(
conn.clone(),
audio_socket,
handshake_result.server_audio_addr,
session_cancel.clone(),
));
// 存储会话
*self.session.write().await = Some(session.clone());
// 启动心跳
self.spawn_heartbeat(session.clone());
// 运行消息循环
self.message_loop(session).await
}
/// 启动心跳任务
fn spawn_heartbeat(&self, session: Arc<Session>) {
let interval = std::time::Duration::from_millis(self.config.heartbeat_ms);
tokio::spawn(async move {
let mut seq = 0;
let mut ticker = tokio::time::interval(interval);
loop {
tokio::select! {
_ = session.cancel.cancelled() => break,
_ = ticker.tick() => {
let t1 = now_us();
let msg = ControlPacket::Ping { client_ts: t1, seq };
if session.send(&msg).await.is_err() {
break;
}
seq += 1;
}
}
}
});
}
/// 消息主循环
async fn message_loop(&self, session: Arc<Session>) -> Result<()> {
let timeout = std::time::Duration::from_secs(self.config.timeout);
loop {
tokio::select! {
_ = session.cancel.cancelled() => break,
result = tokio::time::timeout(timeout, session.recv()) => {
match result {
Ok(Ok(packet)) => {
self.handle_packet(&session, packet).await?;
}
Ok(Err(e)) => {
return Err(anyhow!("Connection error: {}", e));
}
Err(_) => {
return Err(anyhow!("Connection timeout"));
}
}
}
}
}
Ok(())
}
/// 处理控制包
async fn handle_packet(&self, session: &Arc<Session>, packet: ControlPacket) -> Result<()> {
match packet {
ControlPacket::Pong {
client_ts,
server_ts,
..
} => {
let t4 = now_us();
session.update_clock(client_ts, server_ts, t4);
}
ControlPacket::Event { timestamp, data } => {
self.event_bus.receive(data, timestamp, session.id());
}
ControlPacket::RpcResponse { id, result } => {
session.resolve_rpc(id, result);
}
// 处理 RPC 请求(支持超时和异步控制)
ControlPacket::RpcRequest {
id,
run_async,
timeout,
command,
} => {
let session_clone = session.clone();
session
.rpc_manager
.handle_rpc_request(id, run_async, timeout, command, move |pck| {
let s = session_clone.clone();
async move {
return s.send(&pck).await;
}
})
.await?;
}
ControlPacket::StartRecording { config } => {
println!("[Client] Starting recording...");
let handle = RecordPipeline::spawn(
config,
session.audio_socket.clone(),
session.server_audio_addr,
session.cancel.clone(),
);
session.start_recording(handle);
}
ControlPacket::StopRecording => {
println!("[Client] Stopping recording...");
session.stop_recording();
}
ControlPacket::StartPlayback { config } => {
println!("[Client] Starting playback...");
let handle = PlaybackPipeline::spawn(
config,
session.clock.clone(),
session.audio_socket.clone(),
session.cancel.clone(),
);
session.start_playback(handle);
}
ControlPacket::StopPlayback => {
println!("[Client] Stopping playback...");
session.stop_playback();
}
_ => {}
}
Ok(())
}
/// 清理资源
async fn cleanup(&self) {
let mut session_guard = self.session.write().await;
if let Some(session) = session_guard.take() {
session.cleanup();
println!("[Client] Session cleaned up");
}
}
// ==================== 公开 API ====================
/// 执行 RPC 命令
pub async fn rpc(&self, builder: &RpcBuilder) -> Result<CommandResult> {
let session_guard = self.session.read().await;
if let Some(session) = session_guard.as_ref() {
session.rpc(builder).await
} else {
Err(anyhow!("Not connected"))
}
}
/// 发送事件
pub async fn send_event(&self, event: EventData) -> Result<()> {
let session_guard = self.session.read().await;
if let Some(session) = session_guard.as_ref() {
session
.send(&ControlPacket::Event {
timestamp: now_us(),
data: event,
})
.await
} else {
Err(anyhow!("Not connected"))
}
}
/// 检查是否已连接
pub async fn is_connected(&self) -> bool {
self.session.read().await.is_some()
}
/// 关闭客户端
pub fn shutdown(&self) {
self.cancel.cancel();
}
}
@@ -0,0 +1,280 @@
//! # Audio Pipeline - 音频管道抽象
//!
//! 提供录音和播放的管道化处理。
//!
//! ## 设计
//!
//! ```text
//! RecordPipeline:
//! ┌──────────┐ ┌────────────┐ ┌────────────┐
//! │ ALSA Mic │────▶│ Opus Encode│────▶│ UDP Send │
//! │ (thread) │ │ (async) │ │ (async) │
//! └──────────┘ └────────────┘ └────────────┘
//!
//! PlaybackPipeline:
//! ┌────────────┐ ┌────────────┐ ┌────────────┐
//! │ UDP Recv │────▶│ Opus Decode│────▶│ ALSA Play │
//! │ (async) │ │ (async) │ │ (thread) │
//! └────────────┘ └────────────┘ └────────────┘
//! ```
use crate::audio::codec::OpusCodec;
use crate::audio::config::AudioConfig;
use crate::audio::player::AudioPlayer;
use crate::audio::recorder::AudioRecorder;
use crate::net::jitter_buffer::{JitterBuffer, JitterConfig};
use crate::net::network::AudioSocket;
use crate::net::protocol::AudioPacket;
use crate::net::sync::now_us;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
/// 管道句柄 - 用于控制正在运行的音频管道
pub struct PipelineHandle {
cancel: CancellationToken,
}
impl PipelineHandle {
fn new(cancel: CancellationToken) -> Self {
Self { cancel }
}
/// 停止管道
pub fn stop(&self) {
self.cancel.cancel();
}
/// 检查是否已停止
pub fn is_stopped(&self) -> bool {
self.cancel.is_cancelled()
}
}
impl Drop for PipelineHandle {
fn drop(&mut self) {
self.stop();
}
}
/// 录音管道
/// 从麦克风捕获 -> Opus 编码 -> UDP 发送到服务器
pub struct RecordPipeline;
impl RecordPipeline {
/// 启动录音管道
///
/// # Arguments
/// * `config` - 音频配置
/// * `socket` - UDP socket
/// * `target` - 目标服务器地址
/// * `parent_cancel` - 父级取消令牌
pub fn spawn(
config: AudioConfig,
socket: Arc<AudioSocket>,
target: SocketAddr,
parent_cancel: CancellationToken,
) -> PipelineHandle {
let cancel = parent_cancel.child_token();
let handle = PipelineHandle::new(cancel.clone());
let token = cancel.clone();
tokio::spawn(async move {
if let Err(e) = Self::run(config, socket, target, token).await {
eprintln!("[RecordPipeline] Error: {}", e);
}
});
handle
}
async fn run(
config: AudioConfig,
socket: Arc<AudioSocket>,
target: SocketAddr,
cancel: CancellationToken,
) -> anyhow::Result<()> {
// 创建 PCM 数据通道
let (pcm_tx, mut pcm_rx) = mpsc::channel::<Vec<i16>>(32);
// 启动 ALSA 录音线程(阻塞 I/O)
let recorder_config = config.clone();
let channels = recorder_config.channels as usize;
std::thread::spawn(move || {
let recorder = match AudioRecorder::new(&recorder_config) {
Ok(r) => r,
Err(e) => {
eprintln!("[RecordPipeline] Failed to create recorder: {}", e);
return;
}
};
let mut buf =
vec![0i16; recorder_config.frame_size * recorder_config.channels as usize];
loop {
match recorder.read(&mut buf) {
Ok(n) if n > 0 => {
let actual_samples = n * channels;
if pcm_tx
.blocking_send(buf[..actual_samples].to_vec())
.is_err()
{
break;
}
}
Ok(_) => {
// 空读取,继续
}
Err(e) => {
eprintln!("[RecordPipeline] Read error: {}", e);
break;
}
}
}
});
// 创建 Opus 编码器
let mut codec = OpusCodec::new(&config)?;
let mut opus_buf = vec![0u8; 4096];
println!("[RecordPipeline] Started -> {}", target);
// 主循环:从录音线程接收 PCM,编码后发送
loop {
tokio::select! {
_ = cancel.cancelled() => break,
pcm = pcm_rx.recv() => {
match pcm {
Some(samples) => {
if let Ok(encoded_len) = codec.encode(&samples, &mut opus_buf) {
let packet = AudioPacket {
seq: 0,
timestamp: 0,
data: opus_buf[..encoded_len].to_vec(),
};
let _ = socket.send(&packet, target).await;
}
}
None => {
// 录音线程已退出
break;
}
}
}
}
}
println!("[RecordPipeline] Stopped");
Ok(())
}
}
/// 播放管道
/// 从 UDP 接收 -> Opus 解码 -> 扬声器播放
pub struct PlaybackPipeline;
impl PlaybackPipeline {
/// 启动播放管道
///
/// # Arguments
/// * `config` - 音频配置
/// * `socket` - UDP socket
/// * `parent_cancel` - 父级取消令牌
pub fn spawn(
config: AudioConfig,
clock: Arc<parking_lot::Mutex<crate::net::sync::ClockSync>>,
socket: Arc<AudioSocket>,
parent_cancel: CancellationToken,
) -> PipelineHandle {
let cancel = parent_cancel.child_token();
let handle = PipelineHandle::new(cancel.clone());
let token = cancel.clone();
tokio::spawn(async move {
if let Err(e) = Self::run(config, clock, socket, token).await {
eprintln!("[PlaybackPipeline] Error: {}", e);
}
});
handle
}
async fn run(
config: AudioConfig,
clock: Arc<parking_lot::Mutex<crate::net::sync::ClockSync>>,
socket: Arc<AudioSocket>,
cancel: CancellationToken,
) -> anyhow::Result<()> {
// 1. 创建 PCM 通道
let (pcm_tx, mut pcm_rx) = mpsc::channel::<Vec<i16>>(64);
// 2. 专用播放线程
let player_config = config.clone();
std::thread::spawn(move || {
let player = match AudioPlayer::new(&player_config) {
Ok(p) => p,
Err(e) => return eprintln!("[PlaybackPipeline] Player init error: {}", e),
};
// 当 pcm_tx 在异步任务中被 drop,这里会自动退出
while let Some(samples) = pcm_rx.blocking_recv() {
if let Err(e) = player.write(&samples) {
eprintln!("[PlaybackPipeline] Write error: {}", e);
break;
}
}
println!("[PlaybackPipeline] Player thread exited naturally");
});
// 3. Opus 解码器与 Jitter Buffer
let mut codec = OpusCodec::new(&config)?;
let mut jitter_buffer = JitterBuffer::new(JitterConfig::default());
let mut pcm_frame = vec![0i16; config.frame_size * config.channels as usize];
// 提高定时精度
let mut ticker = tokio::time::interval(std::time::Duration::from_millis(5));
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
println!("[PlaybackPipeline] Running");
// 4. 主逻辑循环
// 使用 loop + select,当 cancel 触发时直接 break
let mut udp_buf = vec![0u8; 4096];
loop {
tokio::select! {
// 优先级 1: 外部取消
_ = cancel.cancelled() => {
break;
}
// 优先级 2: 网络接收
result = socket.recv(&mut udp_buf) => {
if let Ok((packet,_)) = result {
let arrival_time = clock.lock().to_server_time(now_us());
jitter_buffer.push(packet, arrival_time);
}
}
// 优先级 3: 播放调度
_ = ticker.tick() => {
let current_time = clock.lock().to_server_time(now_us());
while let Some(packet) = jitter_buffer.pop(current_time) {
if let Ok(samples) = codec.decode(&packet.data, &mut pcm_frame) {
let samples = pcm_frame[..samples * config.channels as usize].to_vec();
if pcm_tx.try_send(samples).is_err() {
break;
}
}
}
}
}
}
println!("[PlaybackPipeline] Stopped");
Ok(())
}
}
@@ -0,0 +1,280 @@
//! # Client Session - 客户端会话管理
//!
//! 轻量级的会话结构,负责:
//! - TCP 控制连接
//! - RPC 管理
//! - 会话生命周期
//! - 活动音频流追踪
use crate::net::command::CommandResult;
use crate::net::network::{AudioSocket, Connection};
use crate::net::protocol::{ClientInfo, ControlPacket};
use crate::net::rpc::{RpcBuilder, RpcManager};
use crate::net::sync::ClockSync;
use anyhow::{Context, Result, anyhow};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use super::{ClientConfig, pipeline::PipelineHandle};
/// 活动管道追踪
pub struct ActivePipelines {
pub recorder: Option<PipelineHandle>,
pub player: Option<PipelineHandle>,
}
impl Default for ActivePipelines {
fn default() -> Self {
Self {
recorder: None,
player: None,
}
}
}
impl ActivePipelines {
pub fn stop_all(&mut self) {
if let Some(h) = self.recorder.take() {
h.stop();
}
if let Some(h) = self.player.take() {
h.stop();
}
}
pub fn start_recording(&mut self, handle: PipelineHandle) {
if let Some(h) = self.recorder.take() {
h.stop();
}
self.recorder = Some(handle);
}
pub fn stop_recording(&mut self) {
if let Some(h) = self.recorder.take() {
h.stop();
}
}
pub fn start_playback(&mut self, handle: PipelineHandle) {
if let Some(h) = self.player.take() {
h.stop();
}
self.player = Some(handle);
}
pub fn stop_playback(&mut self) {
if let Some(h) = self.player.take() {
h.stop();
}
}
pub fn is_recording(&self) -> bool {
self.recorder.is_some()
}
pub fn is_playing(&self) -> bool {
self.player.is_some()
}
}
/// 客户端会话
pub struct Session {
/// TCP 控制连接
pub conn: Arc<Connection>,
/// UDP 音频 socket
pub audio_socket: Arc<AudioSocket>,
/// 服务器音频地址
pub server_audio_addr: SocketAddr,
/// RPC 管理器
pub rpc_manager: Arc<RpcManager>,
/// 会话取消令牌
pub cancel: CancellationToken,
/// 时间同步
pub clock: Arc<parking_lot::Mutex<ClockSync>>,
/// 活动管道
pipelines: parking_lot::Mutex<ActivePipelines>,
/// 会话创建时间
created_at: std::time::Instant,
/// 当前音量
volume: parking_lot::Mutex<u8>,
}
impl Session {
/// 创建新会话
pub fn new(
conn: Arc<Connection>,
audio_socket: Arc<AudioSocket>,
server_audio_addr: SocketAddr,
cancel: CancellationToken,
) -> Self {
Self {
conn,
audio_socket,
server_audio_addr,
rpc_manager: Arc::new(RpcManager::new()),
cancel,
clock: Arc::new(parking_lot::Mutex::new(ClockSync::new(100))),
pipelines: parking_lot::Mutex::new(ActivePipelines::default()),
created_at: std::time::Instant::now(),
volume: parking_lot::Mutex::new(100),
}
}
/// 获取会话 ID(使用 TCP 地址)
pub fn id(&self) -> SocketAddr {
self.conn.peer_addr()
}
/// 检查会话是否仍然有效
pub fn is_alive(&self) -> bool {
!self.cancel.is_cancelled()
}
/// 获取会话运行时间
pub fn uptime_secs(&self) -> u64 {
self.created_at.elapsed().as_secs()
}
/// 发送控制包
pub async fn send(&self, packet: &ControlPacket) -> Result<()> {
self.conn.send(packet).await
}
/// 接收控制包
pub async fn recv(&self) -> Result<ControlPacket> {
self.conn.recv().await
}
/// 更新时间
pub fn update_clock(&self, client_send_ts: u128, server_ts: u128, client_recv_ts: u128) {
self.clock
.lock()
.update(client_send_ts, server_ts, client_recv_ts);
}
/// 发起 RPC 调用(支持超时和异步控制)
pub async fn rpc(&self, request: &RpcBuilder) -> Result<CommandResult> {
let result = self
.rpc_manager
.call(request, |pck| async move {
return self.conn.send(&pck).await;
})
.await?;
Ok(result)
}
/// 处理 RPC 响应
pub fn resolve_rpc(&self, id: u32, result: CommandResult) {
self.rpc_manager.resolve(id, result);
}
/// 开始录音管道
pub fn start_recording(&self, handle: PipelineHandle) {
self.pipelines.lock().start_recording(handle);
}
/// 停止录音管道
pub fn stop_recording(&self) {
self.pipelines.lock().stop_recording();
}
/// 开始播放管道
pub fn start_playback(&self, handle: PipelineHandle) {
self.pipelines.lock().start_playback(handle);
}
/// 停止播放管道
pub fn stop_playback(&self) {
self.pipelines.lock().stop_playback();
}
/// 检查是否正在录音
pub fn is_recording(&self) -> bool {
self.pipelines.lock().is_recording()
}
/// 检查是否正在播放
pub fn is_playing(&self) -> bool {
self.pipelines.lock().is_playing()
}
/// 获取当前音量
pub fn volume(&self) -> u8 {
*self.volume.lock()
}
/// 设置音量
pub fn set_volume(&self, vol: u8) -> u8 {
let mut v = self.volume.lock();
let prev = *v;
*v = vol.min(100);
prev
}
/// 清理所有资源
pub fn cleanup(&self) {
self.cancel.cancel();
self.pipelines.lock().stop_all();
}
}
impl Drop for Session {
fn drop(&mut self) {
self.cleanup();
}
}
/// 握手结果
pub struct HandshakeResult {
pub server_audio_addr: SocketAddr,
}
/// 执行客户端握手
pub async fn handshake(
conn: &Connection,
audio_port: u16,
server_addr: SocketAddr,
config: &ClientConfig,
) -> Result<HandshakeResult> {
// 发送 ClientHello
conn.send(&ControlPacket::ClientHello {
udp_port: audio_port,
auth: config.server_auth.clone(),
version: config.version.clone(),
info: ClientInfo {
model: config.model.clone(),
serial_number: config.serial_number.clone(),
},
})
.await?;
// 等待 ServerHello
let server_udp_port = match conn.recv().await? {
ControlPacket::ServerHello {
version: v,
udp_port,
auth,
} => {
if v != config.version {
return Err(anyhow!("Server version mismatch"));
}
if auth != config.client_auth {
return Err(anyhow!("Invalid server auth"));
}
udp_port
}
_ => return Err(anyhow!("Expected ServerHello")),
};
let server_audio_addr = SocketAddr::new(server_addr.ip(), server_udp_port);
Ok(HandshakeResult { server_audio_addr })
}
+2
View File
@@ -0,0 +1,2 @@
pub mod client;
pub mod server;
@@ -0,0 +1,231 @@
//! # AudioBus - 音频总线
//!
//! 核心的音频路由模块,采用发布-订阅模式处理多客户端音频流。
//!
//! ## 设计理念
//!
//! ```text
//! ┌──────────────────────────────────────────┐
//! │ AudioBus │
//! │ │
//! Client A ──UDP──▶│ ┌─────────┐ ┌──────────────────┐ │
//! │ │ Ingress │──────▶ BroadcastChannel │ │──▶ Client B, C...
//! Client B ──UDP──▶│ │ Router │ └──────────────────┘ │
//! │ └─────────┘ │
//! │ │ │
//! │ ▼ │
//! │ ┌─────────────┐ │
//! │ │ Recorder │──▶ WAV File │
//! │ └─────────────┘ │
//! │ │
//! │ ┌─────────────┐ │
//! │ │ Playback │◀── WAV/MP3 File │──▶ Client X
//! │ └─────────────┘ │
//! └──────────────────────────────────────────┘
//! ```
//!
//! ## 核心概念
//!
//! - **Subscriber**: 订阅者,可以是客户端或录音器
//! - **Publisher**: 发布者,可以是客户端麦克风或文件播放器
//! - **Channel**: 频道,用于隔离不同的音频流组
use crate::net::network::AudioSocket;
use crate::net::protocol::AudioPacket;
use dashmap::DashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::sync::broadcast;
/// 订阅者 ID
pub type SubscriberId = u64;
/// 音频包及其来源
#[derive(Clone, Debug)]
pub struct AudioFrame {
/// 音频数据包
pub packet: AudioPacket,
/// 发送者地址(如果来自网络)
pub source: Option<SocketAddr>,
/// 时间戳(单调递增)
pub timestamp: u64,
}
/// 订阅者信息
#[derive(Clone)]
pub struct Subscriber {
pub id: SubscriberId,
/// UDP 目标地址
pub addr: SocketAddr,
/// 是否过滤自己的音频(防止回声)
pub filter_self: bool,
}
/// 音频总线 - 负责音频流的路由和分发
pub struct AudioBus {
/// UDP socket 用于收发音频
socket: Arc<AudioSocket>,
/// 广播通道 - 发布订阅模式的核心
/// 所有接收到的音频都会广播到这个 channel
broadcast_tx: broadcast::Sender<AudioFrame>,
/// 订阅者注册表
/// key: SocketAddr (UDP 地址)
/// value: Subscriber
subscribers: DashMap<SocketAddr, Subscriber>,
/// 地址到订阅者 ID 的反向映射
addr_to_id: DashMap<SocketAddr, SubscriberId>,
/// ID 生成器
next_id: AtomicU64,
/// 全局时间戳
timestamp: AtomicU64,
}
impl AudioBus {
/// 创建新的音频总线
pub async fn new() -> anyhow::Result<Self> {
let socket = Arc::new(AudioSocket::bind().await?);
// 广播 channel 容量,设置较大以容纳多个订阅者
let (broadcast_tx, _) = broadcast::channel(256);
Ok(Self {
socket,
broadcast_tx,
subscribers: DashMap::new(),
addr_to_id: DashMap::new(),
next_id: AtomicU64::new(1),
timestamp: AtomicU64::new(0),
})
}
/// 获取 UDP 端口
pub fn port(&self) -> u16 {
self.socket.port()
}
/// 获取 socket 引用(用于外部发送)
pub fn socket(&self) -> Arc<AudioSocket> {
self.socket.clone()
}
/// 注册一个订阅者
pub fn register(&self, addr: SocketAddr, filter_self: bool) -> SubscriberId {
let id = self.next_id.fetch_add(1, Ordering::SeqCst);
let subscriber = Subscriber {
id,
addr,
filter_self,
};
self.subscribers.insert(addr, subscriber);
self.addr_to_id.insert(addr, id);
println!("[AudioBus] Subscriber {id} registered at {addr}");
id
}
/// 注销订阅者
pub fn unregister(&self, addr: &SocketAddr) {
if let Some((_, sub)) = self.subscribers.remove(addr) {
self.addr_to_id.remove(addr);
println!("[AudioBus] Subscriber {} unregistered", sub.id);
}
}
/// 订阅广播频道,返回一个 Receiver
pub fn subscribe(&self) -> broadcast::Receiver<AudioFrame> {
self.broadcast_tx.subscribe()
}
/// 发布音频帧到总线
pub fn publish(&self, packet: AudioPacket, source: Option<SocketAddr>) {
let ts = self.timestamp.fetch_add(1, Ordering::SeqCst);
let frame = AudioFrame {
packet,
source,
timestamp: ts,
};
// 忽略没有订阅者的情况
let _ = self.broadcast_tx.send(frame);
}
/// 广播音频帧到所有订阅者(除了发送者自己)
pub async fn broadcast(&self, frame: &AudioFrame) {
for entry in self.subscribers.iter() {
let sub = entry.value();
// 如果启用了自过滤,跳过发送者自己
if sub.filter_self {
if let Some(src) = &frame.source {
if src == &sub.addr {
continue;
}
}
}
// 发送音频包
if let Err(e) = self.socket.send(&frame.packet, sub.addr).await {
eprintln!("[AudioBus] Failed to send to {}: {}", sub.addr, e);
}
}
}
/// 发送音频包到指定地址
pub async fn send_to(&self, packet: &AudioPacket, addr: SocketAddr) -> anyhow::Result<()> {
self.socket.send(packet, addr).await
}
/// 启动 UDP 接收循环
/// 这是一个独立的任务,负责:
/// 1. 接收 UDP 音频包
/// 2. 发布到广播频道
pub async fn run_receiver(&self) {
let mut buf = vec![0u8; 4096];
loop {
match self.socket.recv(&mut buf).await {
Ok((packet, src_addr)) => {
// 只处理已注册的发送者
if self.subscribers.contains_key(&src_addr) {
self.publish(packet, Some(src_addr));
}
}
Err(e) => {
eprintln!("[AudioBus] Recv error: {}", e);
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
}
}
}
/// 启动广播分发循环
/// 订阅广播频道并将音频转发给所有订阅者
pub async fn run_broadcaster(self: Arc<Self>) {
let mut rx = self.subscribe();
loop {
match rx.recv().await {
Ok(frame) => {
self.broadcast(&frame).await;
}
Err(broadcast::error::RecvError::Lagged(n)) => {
eprintln!("[AudioBus] Broadcaster lagged {} frames", n);
}
Err(broadcast::error::RecvError::Closed) => {
break;
}
}
}
}
/// 获取当前订阅者数量
pub fn subscriber_count(&self) -> usize {
self.subscribers.len()
}
/// 检查地址是否已注册
pub fn is_registered(&self, addr: &SocketAddr) -> bool {
self.subscribers.contains_key(addr)
}
}
+428
View File
@@ -0,0 +1,428 @@
//! # Server 模块
//!
//! 实时音频流服务器,支持:
//! - 多客户端连接管理
//! - 音频流发布-订阅
//! - 录音和播放
//! - RPC 远程调用
//! - 实时事件推送
//!
//! ## 架构
//!
//! ```text
//! ┌─────────────────────────────────────────────┐
//! │ Server │
//! │ │
//! Client 1 ──TCP────────┼──▶ SessionManager │
//! Client 2 ──TCP────────┼──▶ ├─ Session 1 │
//! Client N ──TCP────────┼──▶ ├─ Session 2 │
//! │ └─ Session N │
//! │ │ │
//! ┌────────────────┼────────────┴────────────────────────┐ │
//! │ │ │ │
//! ▼ │ ▼ │
//! ┌─────────┐ │ ┌─────────────────────────────┐ │
//! │Command │ │ │ AudioBus │ │
//! │Handler │ │ │ │ │
//! └─────────┘ │ │ ┌─────────┐ ┌──────────┐ │ │
//! │ │ │ │Receiver │ │Broadcaster│ │ │
//! ▼ │ │ │ Loop │─▶│ Loop │──┼───┼──▶ All Clients
//! ┌─────────┐ │ │ └─────────┘ └──────────┘ │ │
//! │ Event │ │ └─────────────────────────────┘ │
//! │ Bus │───────────┼─────────────────────────────────────────────┼──▶ Events
//! └─────────┘ │ │
//! └─────────────────────────────────────────────┘
//! ```
mod audio_bus;
mod session;
mod stream;
pub use audio_bus::{AudioBus, AudioFrame};
pub use session::{Session, SessionManager};
pub use stream::{FilePlaybackStream, RecorderStream, StreamHandle};
use crate::audio::config::AudioConfig;
use crate::audio::wav::WavReader;
use crate::net::command::CommandResult;
use crate::net::discovery::Discovery;
use crate::net::event::{EventBus, EventBusSubscription, EventData};
use crate::net::network::Connection;
use crate::net::protocol::ControlPacket;
use crate::net::rpc::RpcBuilder;
use crate::net::sync::now_us;
use anyhow::{Context, Result, anyhow};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
/// 服务端配置
#[derive(Debug, Clone)]
pub struct ServerConfig {
/// 版本
pub version: String,
/// 客户端认证
pub client_auth: String,
/// 服务端认证
pub server_auth: String,
/// 连接超时(秒)
pub timeout: u64,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
version: env!("CARGO_PKG_VERSION").to_string(),
server_auth: std::env::var("XIAO_SERVER_AUTH")
.unwrap_or_else(|_| "xiao-server".to_string()),
client_auth: std::env::var("XIAO_CLIENT_AUTH")
.unwrap_or_else(|_| "xiao-client".to_string()),
timeout: 60,
}
}
}
/// 实时音频服务器
pub struct Server {
/// 配置
config: ServerConfig,
/// 会话管理器
sessions: Arc<SessionManager>,
/// 音频总线
audio_bus: Arc<AudioBus>,
/// 服务端事件总线
event_bus: Arc<EventBus>,
/// 服务器取消令牌
cancel: CancellationToken,
/// 服务器启动时间
started_at: std::time::Instant,
}
impl Server {
/// 创建新服务器
pub async fn new(config: ServerConfig) -> Result<Self> {
let audio_bus = Arc::new(AudioBus::new().await?);
Ok(Self {
config,
sessions: Arc::new(SessionManager::new()),
audio_bus,
event_bus: Arc::new(EventBus::default()),
cancel: CancellationToken::new(),
started_at: std::time::Instant::now(),
})
}
/// 订阅事件
pub fn subscribe_events(&self) -> EventBusSubscription {
self.event_bus.subscribe()
}
/// 启动服务器
pub async fn run(self: Arc<Self>, port: u16) -> Result<()> {
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
let addr = listener.local_addr()?;
println!("[Server] Listening on TCP: {}", addr);
println!("[Server] Audio UDP port: {}", self.audio_bus.port());
// 广播服务发现
Discovery::broadcast(port).await?;
// 启动音频总线接收循环
let bus = self.audio_bus.clone();
tokio::spawn(async move {
bus.run_receiver().await;
});
// 启动音频总线广播循环
let bus = self.audio_bus.clone();
tokio::spawn(async move {
bus.run_broadcaster().await;
});
// TCP 连接接受循环
loop {
tokio::select! {
_ = self.cancel.cancelled() => {
println!("[Server] Shutting down...");
break;
}
result = listener.accept() => {
match result {
Ok((stream, addr)) => {
let server = self.clone();
tokio::spawn(async move {
if let Err(e) = server.handle_connection(stream, addr).await {
eprintln!("[Server] Connection {} error: {}", addr, e);
}
});
}
Err(e) => {
eprintln!("[Server] Accept error: {}", e);
}
}
}
}
}
Ok(())
}
/// 处理新连接
async fn handle_connection(
self: Arc<Self>,
stream: tokio::net::TcpStream,
addr: SocketAddr,
) -> Result<()> {
println!("[Server] New connection from {}", addr);
let conn = Arc::new(Connection::new(stream)?);
// --- 握手 ---
let (info, audio_addr) = self.handshake(&conn, addr).await?;
println!(
"[Server] Client identified: {} ({}) audio: {}",
info.model, info.serial_number, audio_addr
);
// --- 创建 Session ---
let session_cancel = self.cancel.child_token();
let session = Arc::new(Session::new(
info.clone(),
conn.clone(),
addr,
audio_addr,
session_cancel.clone(),
));
// 注册到 SessionManager 和 AudioBus
self.sessions.register(session.clone());
self.audio_bus.register(audio_addr, true);
// --- 主循环 ---
let result = self.session_loop(session.clone()).await;
// --- 清理 ---
self.audio_bus.unregister(&audio_addr);
self.sessions.unregister(&addr);
result
}
/// 握手流程
async fn handshake(
&self,
conn: &Arc<Connection>,
addr: SocketAddr,
) -> Result<(crate::net::protocol::ClientInfo, SocketAddr)> {
// 等待客户端 Hello
let (info, client_audio_port) = match conn.recv().await? {
ControlPacket::ClientHello {
auth,
version: v,
udp_port,
info,
} => {
if v != self.config.version {
return Err(anyhow!("Client version mismatch"));
}
if auth != self.config.server_auth {
return Err(anyhow!("Invalid client auth"));
}
(info, udp_port)
}
_ => return Err(anyhow!("Expected ClientHello")),
};
// 发送服务器 Hello
conn.send(&ControlPacket::ServerHello {
auth: self.config.client_auth.clone(),
version: self.config.version.clone(),
udp_port: self.audio_bus.port(),
})
.await?;
let audio_addr = SocketAddr::new(addr.ip(), client_audio_port);
Ok((info, audio_addr))
}
/// Session 消息循环
async fn session_loop(&self, session: Arc<Session>) -> Result<()> {
let timeout = std::time::Duration::from_secs(self.config.timeout);
loop {
tokio::select! {
_ = session.cancel.cancelled() => break,
result = tokio::time::timeout(timeout, session.recv()) => {
match result {
Ok(Ok(packet)) => {
self.handle_packet(&session, packet).await?;
}
Ok(Err(e)) => {
return Err(anyhow!("Connection error: {}", e));
}
Err(_) => {
return Err(anyhow!("Connection timeout"));
}
}
}
}
}
Ok(())
}
/// 处理控制包
async fn handle_packet(&self, session: &Arc<Session>, packet: ControlPacket) -> Result<()> {
match packet {
ControlPacket::Ping { client_ts, seq } => {
let pong = ControlPacket::Pong {
client_ts,
server_ts: now_us(),
seq,
};
session.send(&pong).await?;
}
ControlPacket::Event { timestamp, data } => {
self.event_bus.receive(data, timestamp, session.id());
}
ControlPacket::RpcResponse { id, result } => {
session.resolve_rpc(id, result);
}
ControlPacket::RpcRequest {
id,
run_async,
timeout,
command,
} => {
let session_clone = session.clone();
session
.rpc_manager
.handle_rpc_request(id, run_async, timeout, command, move |pck| {
let s = session_clone.clone();
async move {
return s.send(&pck).await;
}
})
.await?;
}
_ => {}
}
Ok(())
}
// ==================== 公开 API ====================
/// 获取所有已连接的客户端地址
pub async fn get_clients(&self) -> Vec<SocketAddr> {
self.sessions.all_addrs()
}
/// 获取客户端数量
pub fn client_count(&self) -> usize {
self.sessions.count()
}
/// 获取服务器运行时间
pub fn uptime_secs(&self) -> u64 {
self.started_at.elapsed().as_secs()
}
/// 向客户端发起 RPC
pub async fn rpc(&self, addr: SocketAddr, builder: &RpcBuilder) -> Result<CommandResult> {
let session = self.sessions.get(&addr).context("Session not found")?;
session.rpc(builder).await
}
/// 发送事件
pub async fn send_event(&self, addr: SocketAddr, event: EventData) -> Result<()> {
let session = self.sessions.get(&addr).context("Session not found")?;
session
.send(&ControlPacket::Event {
timestamp: now_us(),
data: event,
})
.await?;
Ok(())
}
/// 开始录音
pub async fn start_record(&self, addr: SocketAddr, config: AudioConfig) -> Result<()> {
let session = self.sessions.get(&addr).context("Session not found")?;
let filename = format!(
"temp/recorded_{}.wav",
session.info.serial_number.replace(":", "")
);
// 创建录音流,订阅音频总线
let handle = RecorderStream::spawn(
config.clone(),
filename.clone(),
self.audio_bus.subscribe(),
Some(session.audio_addr),
session.cancel.clone(),
);
session.start_recording(handle, config.clone());
// 通知客户端开始发送音频
session
.send(&ControlPacket::StartRecording { config })
.await?;
println!("[Server] Recording started for {} -> {}", addr, filename);
Ok(())
}
/// 停止录音
pub async fn stop_record(&self, addr: SocketAddr) -> Result<()> {
let session = self.sessions.get(&addr).context("Session not found")?;
session.stop_recording();
session.send(&ControlPacket::StopRecording).await?;
println!("[Server] Recording stopped for {}", addr);
Ok(())
}
/// 开始播放
pub async fn start_play(&self, addr: SocketAddr, file_path: &str) -> Result<()> {
let session = self.sessions.get(&addr).context("Session not found")?;
let reader = WavReader::open(file_path)?;
// 通知客户端准备接收音频
session
.send(&ControlPacket::StartPlayback {
config: reader.config.clone(),
})
.await?;
// 创建播放流
let handle = FilePlaybackStream::spawn(
reader,
self.audio_bus.socket(),
session.audio_addr,
session.cancel.clone(),
);
session.start_playback(handle);
println!("[Server] Playback started for {} from {}", addr, file_path);
Ok(())
}
/// 停止播放
pub async fn stop_play(&self, addr: SocketAddr) -> Result<()> {
let session = self.sessions.get(&addr).context("Session not found")?;
session.stop_playback();
session.send(&ControlPacket::StopPlayback).await?;
println!("[Server] Playback stopped for {}", addr);
Ok(())
}
/// 关闭服务器
pub fn shutdown(&self) {
self.cancel.cancel();
}
}
@@ -0,0 +1,328 @@
//! # Session - 客户端会话管理
//!
//! 轻量级的会话结构,专注于:
//! - TCP 控制连接
//! - RPC 管理
//! - 会话生命周期
//!
//! 音频流的实际处理由 AudioBus 和 Stream 模块负责。
use crate::audio::config::AudioConfig;
use crate::net::command::CommandResult;
use crate::net::network::Connection;
use crate::net::protocol::{ClientInfo, ControlPacket};
use crate::net::rpc::{RpcBuilder, RpcManager};
use anyhow::{Context, Result};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use super::stream::StreamHandle;
/// 活动流追踪
/// 用于追踪当前 session 的活动音频流
pub struct ActiveStreams {
/// 当前录音流句柄
pub recorder: Option<StreamHandle>,
/// 当前播放流句柄
pub playback: Option<StreamHandle>,
}
impl Default for ActiveStreams {
fn default() -> Self {
Self {
recorder: None,
playback: None,
}
}
}
impl ActiveStreams {
/// 停止所有活动流
pub fn stop_all(&mut self) {
if let Some(h) = self.recorder.take() {
h.stop();
}
if let Some(h) = self.playback.take() {
h.stop();
}
}
/// 开始录音(停止之前的录音)
pub fn start_recording(&mut self, handle: StreamHandle) {
if let Some(h) = self.recorder.take() {
h.stop();
}
self.recorder = Some(handle);
}
/// 停止录音
pub fn stop_recording(&mut self) {
if let Some(h) = self.recorder.take() {
h.stop();
}
}
/// 开始播放(停止之前的播放)
pub fn start_playback(&mut self, handle: StreamHandle) {
if let Some(h) = self.playback.take() {
h.stop();
}
self.playback = Some(handle);
}
/// 停止播放
pub fn stop_playback(&mut self) {
if let Some(h) = self.playback.take() {
h.stop();
}
}
/// 检查是否正在录音
pub fn is_recording(&self) -> bool {
self.recorder.is_some()
}
/// 检查是否正在播放
pub fn is_playing(&self) -> bool {
self.playback.is_some()
}
}
/// 客户端会话
pub struct Session {
/// 客户端信息
pub info: ClientInfo,
/// TCP 控制连接
pub conn: Arc<Connection>,
/// RPC 管理器
pub rpc_manager: Arc<RpcManager>,
/// TCP 地址(用作会话 ID
pub tcp_addr: SocketAddr,
/// UDP 音频地址
pub audio_addr: SocketAddr,
/// 会话取消令牌
pub cancel: CancellationToken,
/// 活动流
streams: parking_lot::Mutex<ActiveStreams>,
/// 当前录音配置(如果正在录音)
recording_config: parking_lot::Mutex<Option<AudioConfig>>,
/// 会话创建时间
created_at: std::time::Instant,
}
impl Session {
/// 创建新会话
pub fn new(
info: ClientInfo,
conn: Arc<Connection>,
tcp_addr: SocketAddr,
audio_addr: SocketAddr,
cancel: CancellationToken,
) -> Self {
Self {
info,
conn,
rpc_manager: Arc::new(RpcManager::new()),
tcp_addr,
audio_addr,
cancel,
streams: parking_lot::Mutex::new(ActiveStreams::default()),
recording_config: parking_lot::Mutex::new(None),
created_at: std::time::Instant::now(),
}
}
/// 获取会话 ID(使用 TCP 地址)
pub fn id(&self) -> SocketAddr {
self.tcp_addr
}
/// 检查会话是否仍然有效
pub fn is_alive(&self) -> bool {
!self.cancel.is_cancelled()
}
/// 获取会话运行时间(秒)
pub fn uptime_secs(&self) -> u64 {
self.created_at.elapsed().as_secs()
}
/// 发送控制包
pub async fn send(&self, packet: &ControlPacket) -> Result<()> {
self.conn.send(packet).await
}
/// 接收控制包
pub async fn recv(&self) -> Result<ControlPacket> {
self.conn.recv().await
}
/// 发起 RPC 调用(支持超时和异步控制)
pub async fn rpc(&self, request: &RpcBuilder) -> Result<CommandResult> {
let result = self
.rpc_manager
.call(request, |pck| async move {
return self.conn.send(&pck).await;
})
.await?;
Ok(result)
}
/// 处理 RPC 响应
pub fn resolve_rpc(&self, id: u32, result: CommandResult) {
self.rpc_manager.resolve(id, result);
}
/// 开始录音流
pub fn start_recording(&self, handle: StreamHandle, config: AudioConfig) {
self.streams.lock().start_recording(handle);
*self.recording_config.lock() = Some(config);
}
/// 停止录音流
pub fn stop_recording(&self) {
self.streams.lock().stop_recording();
*self.recording_config.lock() = None;
}
/// 开始播放流
pub fn start_playback(&self, handle: StreamHandle) {
self.streams.lock().start_playback(handle);
}
/// 停止播放流
pub fn stop_playback(&self) {
self.streams.lock().stop_playback();
}
/// 检查是否正在录音
pub fn is_recording(&self) -> bool {
self.streams.lock().is_recording()
}
/// 检查是否正在播放
pub fn is_playing(&self) -> bool {
self.streams.lock().is_playing()
}
/// 清理所有资源
pub fn cleanup(&self) {
self.cancel.cancel();
self.streams.lock().stop_all();
}
/// 获取当前录音配置
pub fn recording_config(&self) -> Option<AudioConfig> {
self.recording_config.lock().clone()
}
}
impl Drop for Session {
fn drop(&mut self) {
self.cleanup();
}
}
/// 会话管理器
/// 负责管理所有客户端会话
pub struct SessionManager {
sessions: dashmap::DashMap<SocketAddr, Arc<Session>>,
/// 从 UDP 地址到 TCP 地址的映射
udp_to_tcp: dashmap::DashMap<SocketAddr, SocketAddr>,
}
impl SessionManager {
pub fn new() -> Self {
Self {
sessions: dashmap::DashMap::new(),
udp_to_tcp: dashmap::DashMap::new(),
}
}
/// 注册新会话
pub fn register(&self, session: Arc<Session>) {
let tcp_addr = session.tcp_addr;
let audio_addr = session.audio_addr;
self.udp_to_tcp.insert(audio_addr, tcp_addr);
self.sessions.insert(tcp_addr, session);
println!(
"[SessionManager] Registered: {} (audio: {})",
tcp_addr, audio_addr
);
}
/// 注销会话
pub fn unregister(&self, tcp_addr: &SocketAddr) -> Option<Arc<Session>> {
if let Some((_, session)) = self.sessions.remove(tcp_addr) {
self.udp_to_tcp.remove(&session.audio_addr);
session.cleanup();
println!(
"[SessionManager] Unregistered: {} ({})",
tcp_addr, session.info.model
);
Some(session)
} else {
None
}
}
/// 通过 TCP 地址获取会话
pub fn get(&self, tcp_addr: &SocketAddr) -> Option<Arc<Session>> {
self.sessions.get(tcp_addr).map(|r| r.value().clone())
}
/// 通过 UDP 地址获取会话
pub fn get_by_udp(&self, udp_addr: &SocketAddr) -> Option<Arc<Session>> {
self.udp_to_tcp
.get(udp_addr)
.and_then(|tcp_addr| self.get(tcp_addr.value()))
}
/// 获取所有会话地址
pub fn all_addrs(&self) -> Vec<SocketAddr> {
self.sessions.iter().map(|r| *r.key()).collect()
}
/// 获取所有会话
pub fn all_sessions(&self) -> Vec<Arc<Session>> {
self.sessions.iter().map(|r| r.value().clone()).collect()
}
/// 获取会话数量
pub fn count(&self) -> usize {
self.sessions.len()
}
/// 广播控制包到所有会话
pub async fn broadcast(&self, packet: &ControlPacket) {
for entry in self.sessions.iter() {
let _ = entry.value().send(packet).await;
}
}
/// 广播控制包到所有会话(除了指定的)
pub async fn broadcast_except(&self, packet: &ControlPacket, except: &SocketAddr) {
for entry in self.sessions.iter() {
if entry.key() != except {
let _ = entry.value().send(packet).await;
}
}
}
}
impl Default for SessionManager {
fn default() -> Self {
Self::new()
}
}
+304
View File
@@ -0,0 +1,304 @@
//! # AudioStream - 音频流抽象
//!
//! 提供统一的音频流处理接口,支持多种输入源和输出目标。
//!
//! ## 设计
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────────┐
//! │ Stream Types │
//! │ │
//! │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
//! │ │ FileSource │ │ BusSource │ │ NetworkSink │ │
//! │ │ (WAV Reader) │ │ (From Bus) │ │ (To Client) │ │
//! │ └──────────────┘ └──────────────┘ └──────────────┘ │
//! │ │
//! │ ┌──────────────┐ ┌──────────────┐ │
//! │ │ FileSink │ │ BusSink │ │
//! │ │ (WAV Writer) │ │ (To Bus) │ │
//! │ └──────────────┘ └──────────────┘ │
//! └─────────────────────────────────────────────────────────────┘
//! ```
use crate::audio::codec::OpusCodec;
use crate::audio::config::AudioConfig;
use crate::audio::wav::{WavReader, WavWriter};
use crate::net::network::AudioSocket;
use crate::net::protocol::AudioPacket;
use crate::net::sync::now_us;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::broadcast;
use tokio_util::sync::CancellationToken;
use super::audio_bus::AudioFrame;
/// 音频流任务句柄
/// 用于控制正在运行的音频流任务
pub struct StreamHandle {
cancel: CancellationToken,
}
impl StreamHandle {
pub fn new(cancel: CancellationToken) -> Self {
Self { cancel }
}
/// 停止流
pub fn stop(&self) {
self.cancel.cancel();
}
/// 检查是否已停止
pub fn is_stopped(&self) -> bool {
self.cancel.is_cancelled()
}
}
impl Drop for StreamHandle {
fn drop(&mut self) {
self.cancel.cancel();
}
}
/// 音频流发送控制器
pub struct StreamSender {
socket: Arc<AudioSocket>,
target: SocketAddr,
codec: OpusCodec,
// 缓存区:避免重复分配内存
opus_buffer: Vec<u8>,
// 状态变量
seq: u32,
stream_start_ts: Option<u128>,
// 配置参数
input_len: usize,
frame_duration_us: u128,
max_lead_us: u128, // 允许的最大超前时间,例如 500_000 (1s)
}
impl StreamSender {
pub fn new(
config: AudioConfig,
socket: Arc<AudioSocket>,
target: SocketAddr,
) -> anyhow::Result<Self> {
let codec = OpusCodec::new(&config)?;
let input_len: usize = config.frame_size * (config.channels as usize);
// 预分配缓冲区
let opus_buffer = vec![0u8; 4096];
let frame_duration_us =
(config.frame_size as f64 / config.sample_rate as f64 * 1_000_000.0) as u128;
Ok(Self {
socket,
target,
codec,
opus_buffer,
seq: 0,
stream_start_ts: None,
input_len,
frame_duration_us,
max_lead_us: 500_000,
})
}
/// 发送音频帧
pub async fn send(&mut self, pcm_input: &[i16]) -> anyhow::Result<()> {
// 0. 处理输入(不足时填充静音)
let pcm_buffer = if pcm_input.len() < self.input_len {
let mut pcm_buffer = vec![0i16; self.input_len];
pcm_buffer[..pcm_input.len()].copy_from_slice(pcm_input);
pcm_buffer
} else {
pcm_input.to_vec()
};
// 1. 编码
let encoded_len = self.codec.encode(&pcm_buffer, &mut self.opus_buffer)?;
// 2. 时间戳处理
let now = now_us();
let start_ts = *self.stream_start_ts.get_or_insert(now);
let target_ts = start_ts + (self.seq as u128 * self.frame_duration_us);
// 3. 构建并发送
let packet = AudioPacket {
seq: self.seq,
timestamp: target_ts,
data: self.opus_buffer[..encoded_len].to_vec(),
};
self.socket.send(&packet, self.target).await?;
self.seq += 1;
// 4. 平滑流控
// 如果发送进度超过当前时间 + 允许的缓冲量,则进行睡眠
if target_ts > now + self.max_lead_us {
let drift = target_ts - (now + self.max_lead_us);
let sleep_ms = (drift / 1000).min(100) as u64;
if sleep_ms > 0 {
tokio::time::sleep(Duration::from_millis(sleep_ms)).await;
}
}
Ok(())
}
}
/// 文件播放流 - 从 WAV 文件读取并发送到指定客户端
pub struct FilePlaybackStream;
impl FilePlaybackStream {
/// 启动文件播放流
///
/// # Arguments
/// * `config` - 音频配置(用于 Opus 编码)
/// * `reader` - WAV 文件读取器
/// * `socket` - UDP socket
/// * `target` - 目标客户端地址
/// * `parent_cancel` - 父级取消令牌(用于 session 级别取消)
pub fn spawn(
reader: WavReader,
socket: Arc<AudioSocket>,
target: SocketAddr,
parent_cancel: CancellationToken,
) -> StreamHandle {
let cancel = parent_cancel.child_token();
let token = cancel.clone();
tokio::spawn(async move {
if let Err(e) = Self::run(reader, socket, target, token).await {
eprintln!("[FilePlayback] Error: {}", e);
}
});
StreamHandle::new(cancel)
}
async fn run(
mut reader: WavReader,
socket: Arc<AudioSocket>,
target: SocketAddr,
cancel: CancellationToken,
) -> anyhow::Result<()> {
println!("[FilePlayback] Started -> {}", target);
let mut sender = StreamSender::new(reader.config.clone(), socket, target)?;
loop {
tokio::select! {
_ = cancel.cancelled() => break,
result = async {
// 读取一帧 PCM 数据
if let Some(pcm) = reader.read_one_frame()? {
// 编码并发送
sender.send(pcm).await
} else {
Err(anyhow::anyhow!("EOF"))
}
} => {
if let Err(e) = result {
if e.to_string() != "EOF" {
eprintln!("[FilePlayback] Error: {}", e);
}
break;
}
}
}
}
println!("[FilePlayback] Stopped");
Ok(())
}
}
/// 录音流 - 从总线订阅音频并写入 WAV 文件
pub struct RecorderStream;
impl RecorderStream {
/// 启动录音流
///
/// # Arguments
/// * `config` - 音频配置
/// * `filename` - 输出文件路径
/// * `bus_rx` - 音频总线接收器
/// * `source_filter` - 仅录制来自此地址的音频(None 表示全部录制)
/// * `parent_cancel` - 父级取消令牌
pub fn spawn(
config: AudioConfig,
filename: String,
bus_rx: broadcast::Receiver<AudioFrame>,
source_filter: Option<SocketAddr>,
parent_cancel: CancellationToken,
) -> StreamHandle {
let cancel = parent_cancel.child_token();
let token = cancel.clone();
tokio::spawn(async move {
if let Err(e) = Self::run(config, filename, bus_rx, source_filter, token).await {
eprintln!("[Recorder] Error: {}", e);
}
});
StreamHandle::new(cancel)
}
async fn run(
config: AudioConfig,
filename: String,
mut bus_rx: broadcast::Receiver<AudioFrame>,
source_filter: Option<SocketAddr>,
cancel: CancellationToken,
) -> anyhow::Result<()> {
let mut writer = WavWriter::create(&filename, config.sample_rate, config.channels)?;
let mut codec = OpusCodec::new(&config)?;
let mut pcm = vec![0i16; config.frame_size * config.channels as usize];
println!(
"[Recorder] Started -> {} (filter: {:?})",
filename, source_filter
);
loop {
tokio::select! {
_ = cancel.cancelled() => break,
result = bus_rx.recv() => {
match result {
Ok(frame) => {
// 应用源过滤
if let Some(filter_addr) = &source_filter {
if frame.source.as_ref() != Some(filter_addr) {
continue;
}
}
// 解码并写入
if let Ok(samples_per_channel) = codec.decode(&frame.packet.data, &mut pcm) {
let total_samples = samples_per_channel * config.channels as usize;
let _ = writer.write_samples(&pcm[..total_samples])?;
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
eprintln!("[Recorder] Lagged {} frames", n);
}
Err(broadcast::error::RecvError::Closed) => {
break;
}
}
}
}
}
// 确保正确关闭文件
writer.finalize()?;
println!("[Recorder] Stopped, file saved: {}", filename);
Ok(())
}
}
+65
View File
@@ -0,0 +1,65 @@
use crate::audio::config::{AudioConfig, AudioScene};
use anyhow::{Context, Result};
use opus::{Application, Bitrate, Channels, Decoder, Encoder};
pub struct OpusCodec {
encoder: Encoder,
decoder: Decoder,
}
impl OpusCodec {
pub fn new(config: &AudioConfig) -> Result<Self> {
let opus_rate = match config.sample_rate {
16000 => 16000,
48000 => 48000,
_ => {
return Err(anyhow::anyhow!(
"Unsupported sample rate for Opus: {}",
config.sample_rate
));
}
};
let channels = match config.channels {
1 => Channels::Mono,
2 => Channels::Stereo,
_ => return Err(anyhow::anyhow!("Unsupported channels: {}", config.channels)),
};
let mode = match config.audio_scene {
AudioScene::Music => Application::Audio,
AudioScene::Voice => Application::Voip,
};
let mut encoder =
Encoder::new(opus_rate, channels, mode).context("Opus encoder init failed")?;
let bitrate = if config.bitrate <= 0 {
Bitrate::Auto
} else {
Bitrate::Bits(config.bitrate)
};
encoder.set_bitrate(bitrate)?;
encoder.set_vbr(config.vbr)?;
if config.fec {
encoder.set_inband_fec(true)?;
encoder.set_packet_loss_perc(10)?;
}
let decoder =
Decoder::new(config.sample_rate, channels).context("Opus decoder init failed")?;
Ok(Self { encoder, decoder })
}
pub fn encode(&mut self, pcm: &[i16], out: &mut [u8]) -> Result<usize> {
self.encoder.encode(pcm, out).context("Opus encode failed")
}
pub fn decode(&mut self, opus: &[u8], out: &mut [i16]) -> Result<usize> {
self.decoder
.decode(opus, out, false)
.context("Opus decode failed")
}
}
+56
View File
@@ -0,0 +1,56 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AudioScene {
Music,
Voice,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioConfig {
pub capture_device: String,
pub playback_device: String,
pub sample_rate: u32,
pub channels: u16,
pub frame_size: usize,
pub audio_scene: AudioScene,
pub bitrate: i32,
pub vbr: bool,
pub fec: bool,
}
impl AudioConfig {
pub fn voice_16k() -> Self {
Self {
capture_device: "plug:Capture".to_string(),
playback_device: "default".to_string(),
sample_rate: 16_000,
channels: 1,
frame_size: 320, // 20ms
audio_scene: AudioScene::Voice,
bitrate: 32_000,
vbr: true,
fec: false,
}
}
pub fn music_48k() -> Self {
Self {
capture_device: "plug:Capture".to_string(),
playback_device: "default".to_string(),
sample_rate: 48_000,
channels: 2,
frame_size: 960, // 20ms
audio_scene: AudioScene::Music,
bitrate: 320_000,
vbr: true,
fec: false,
}
}
}
impl Default for AudioConfig {
fn default() -> Self {
Self::voice_16k()
}
}
+6
View File
@@ -0,0 +1,6 @@
pub mod codec;
pub mod config;
pub mod player;
pub mod recorder;
pub mod wav;
pub mod reader;
+58
View File
@@ -0,0 +1,58 @@
use crate::audio::config::AudioConfig;
use anyhow::{Context, Result, anyhow};
pub struct AudioPlayer {
#[cfg(target_os = "linux")]
pcm: alsa::pcm::PCM,
}
impl AudioPlayer {
pub fn new(config: &AudioConfig) -> Result<Self> {
#[cfg(target_os = "linux")]
{
use alsa::Direction;
use alsa::pcm::{Access, Format, HwParams, PCM};
let pcm = PCM::new(&config.playback_device, Direction::Playback, false)
.context("Failed to open playback PCM device")?;
{
let hwp = HwParams::any(&pcm).context("Failed to get HwParams")?;
hwp.set_access(Access::RWInterleaved)?;
hwp.set_format(Format::s16())?;
hwp.set_rate(config.sample_rate, alsa::ValueOr::Nearest)?;
hwp.set_channels(config.channels as u32)?;
pcm.hw_params(&hwp)?;
pcm.prepare().context("Failed to prepare PCM")?;
}
Ok(Self { pcm })
}
#[cfg(not(target_os = "linux"))]
{
Err(anyhow!("Linux Only"))
}
}
pub fn write(&self, buf: &[i16]) -> Result<usize> {
#[cfg(target_os = "linux")]
{
let res = self.pcm.io_i16()?.writei(buf);
match res {
Ok(n) => Ok(n),
Err(e) if e.errno() == 32 => {
println!("ALSA write underrun, preparing PCM");
// Broken pipe (underrun)
self.pcm.prepare()?;
self.pcm
.io_i16()?
.writei(buf)
.context("ALSA write retry failed")
}
Err(e) => Err(e.into()),
}
}
#[cfg(not(target_os = "linux"))]
{
Err(anyhow!("Linux Only"))
}
}
}
+143
View File
@@ -0,0 +1,143 @@
#![cfg(not(target_os = "linux"))]
use anyhow::{Context, Result};
use std::fs::File;
use std::path::Path;
use symphonia::core::audio::SampleBuffer;
use symphonia::core::codecs::{Decoder, DecoderOptions};
use symphonia::core::formats::{FormatOptions, FormatReader};
use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;
pub struct AudioReader {
format: Box<dyn FormatReader>,
decoder: Box<dyn Decoder>,
track_id: u32,
sample_buf: Option<SampleBuffer<i16>>,
left_buffer: Vec<i16>,
right_buffer: Vec<i16>,
pub channels: usize,
pub sample_rate: u32,
}
impl AudioReader {
pub fn new(path: impl AsRef<Path>) -> Result<Self> {
let path_ref = path.as_ref();
let src =
File::open(path_ref).context(format!("Failed to open audio file: {:?}", path_ref))?;
let mss = MediaSourceStream::new(Box::new(src), Default::default());
let mut hint = Hint::new();
if let Some(ext) = path_ref.extension().and_then(|s| s.to_str()) {
hint.with_extension(ext);
}
let probed = symphonia::default::get_probe()
.format(
&hint,
mss,
&FormatOptions::default(),
&MetadataOptions::default(),
)
.context("Failed to probe audio format")?;
let format = probed.format;
let track = format
.tracks()
.iter()
.find(|t| t.codec_params.codec != symphonia::core::codecs::CODEC_TYPE_NULL)
.context("No supported audio track found")?;
let track_id = track.id;
let decoder = symphonia::default::get_codecs()
.make(&track.codec_params, &DecoderOptions::default())
.context("Failed to create decoder")?;
let channels = track.codec_params.channels.map(|c| c.count()).unwrap_or(1);
let sample_rate = track.codec_params.sample_rate.unwrap_or(44100);
Ok(Self {
format,
decoder,
track_id,
sample_buf: None,
channels,
sample_rate,
left_buffer: Vec::new(),
right_buffer: Vec::new(),
})
}
pub fn read_chunk(&mut self, chunk_size: usize) -> Result<Option<(Vec<i16>, Vec<i16>)>> {
while self.left_buffer.len() < chunk_size {
if let Some((l, r)) = self.read_frame_internal()? {
self.left_buffer.extend(l);
self.right_buffer.extend(r);
} else {
break;
}
}
if self.left_buffer.is_empty() {
return Ok(None);
}
let actual_size = std::cmp::min(chunk_size, self.left_buffer.len());
let left = self.left_buffer.drain(0..actual_size).collect();
let right = self.right_buffer.drain(0..actual_size).collect();
Ok(Some((left, right)))
}
fn read_frame_internal(&mut self) -> Result<Option<(Vec<i16>, Vec<i16>)>> {
loop {
let packet = match self.format.next_packet() {
Ok(packet) => packet,
Err(symphonia::core::errors::Error::IoError(ref e))
if e.kind() == std::io::ErrorKind::UnexpectedEof =>
{
return Ok(None);
}
Err(e) => return Err(e.into()),
};
if packet.track_id() != self.track_id {
continue;
}
let decoded = self
.decoder
.decode(&packet)
.context("Failed to decode packet")?;
if self.sample_buf.is_none() {
self.sample_buf = Some(SampleBuffer::<i16>::new(
decoded.capacity() as u64,
*decoded.spec(),
));
}
if let Some(buf) = self.sample_buf.as_mut() {
buf.copy_interleaved_ref(decoded);
let samples = buf.samples();
let mut left = Vec::with_capacity(samples.len() / self.channels);
let mut right = Vec::with_capacity(samples.len() / self.channels);
if self.channels == 2 {
for i in (0..samples.len()).step_by(2) {
left.push(samples[i]);
right.push(samples[i + 1]);
}
} else {
for &s in samples {
left.push(s);
right.push(s);
}
}
return Ok(Some((left, right)));
}
}
}
}
+56
View File
@@ -0,0 +1,56 @@
use crate::audio::config::AudioConfig;
use anyhow::{Context, Result, anyhow};
pub struct AudioRecorder {
#[cfg(target_os = "linux")]
pcm: alsa::pcm::PCM,
}
impl AudioRecorder {
pub fn new(config: &AudioConfig) -> Result<Self> {
#[cfg(target_os = "linux")]
{
use alsa::Direction;
use alsa::pcm::{Access, Format, HwParams, PCM};
let pcm = PCM::new(&config.capture_device, Direction::Capture, false)?;
{
let hwp = HwParams::any(&pcm)?;
hwp.set_access(Access::RWInterleaved)?;
hwp.set_format(Format::s16())?;
hwp.set_rate_near(config.sample_rate, alsa::ValueOr::Nearest)?;
hwp.set_channels_near(config.channels as u32)?;
pcm.hw_params(&hwp)?;
}
pcm.prepare()?;
Ok(Self { pcm })
}
#[cfg(not(target_os = "linux"))]
{
Err(anyhow!("Linux Only"))
}
}
pub fn read(&self, buf: &mut [i16]) -> Result<usize> {
#[cfg(target_os = "linux")]
{
match self.pcm.io_i16()?.readi(buf) {
Ok(n) => Ok(n),
Err(e) if e.errno() == 32 => {
// 32 = Broken pipe (Overrun)
println!("ALSA recording overrun, recovering...");
self.pcm.prepare()?;
self.pcm
.io_i16()?
.readi(buf)
.context("ALSA read retry failed")
}
Err(e) => Err(e.into()),
}
}
#[cfg(not(target_os = "linux"))]
{
Err(anyhow!("Linux Only"))
}
}
}
+131
View File
@@ -0,0 +1,131 @@
use crate::audio::config::AudioConfig;
use anyhow::Result;
use std::fs::File;
use std::io::{BufWriter, Seek, SeekFrom, Write};
pub struct WavWriter {
writer: BufWriter<File>,
data_size: u32,
sample_rate: u32,
channels: u16,
}
impl WavWriter {
pub fn create(path: &str, sample_rate: u32, channels: u16) -> Result<Self> {
let file = File::create(path)?;
let mut writer = BufWriter::new(file);
// Header placeholder
writer.write_all(&[0u8; 44])?;
Ok(Self {
writer,
data_size: 0,
sample_rate,
channels,
})
}
pub fn write_samples(&mut self, samples: &[i16]) -> Result<()> {
for &sample in samples {
self.writer.write_all(&sample.to_le_bytes())?;
self.data_size += 2;
}
Ok(())
}
pub fn finalize(mut self) -> Result<()> {
self.writer.flush()?;
let mut file = self.writer.into_inner()?;
file.seek(SeekFrom::Start(0))?;
let file_size = 36 + self.data_size;
let byte_rate = self.sample_rate * self.channels as u32 * 2;
let block_align = self.channels * 2;
let mut header = [0u8; 44];
header[0..4].copy_from_slice(b"RIFF");
header[4..8].copy_from_slice(&file_size.to_le_bytes());
header[8..12].copy_from_slice(b"WAVE");
header[12..16].copy_from_slice(b"fmt ");
header[16..20].copy_from_slice(&16u32.to_le_bytes());
header[20..22].copy_from_slice(&1u16.to_le_bytes()); // PCM
header[22..24].copy_from_slice(&self.channels.to_le_bytes());
header[24..28].copy_from_slice(&self.sample_rate.to_le_bytes());
header[28..32].copy_from_slice(&byte_rate.to_le_bytes());
header[32..34].copy_from_slice(&block_align.to_le_bytes());
header[34..36].copy_from_slice(&16u16.to_le_bytes()); // bits per sample
header[36..40].copy_from_slice(b"data");
header[40..44].copy_from_slice(&self.data_size.to_le_bytes());
file.write_all(&header)?;
Ok(())
}
}
pub struct WavReader {
#[cfg(not(target_os = "linux"))]
reader: crate::audio::reader::AudioReader,
pcm_buffer: Vec<i16>,
pub config: AudioConfig,
}
impl WavReader {
pub fn open(path: &str) -> Result<Self> {
#[cfg(not(target_os = "linux"))]
{
let reader = crate::audio::reader::AudioReader::new(path)?;
let channels = reader.channels as u16;
let config = AudioConfig {
channels,
sample_rate: reader.sample_rate,
frame_size: reader.sample_rate as usize / 50, // 20ms
..AudioConfig::music_48k()
};
let input_len = config.frame_size * (channels as usize);
Ok(Self {
reader,
config,
pcm_buffer: vec![0i16; input_len],
})
}
#[cfg(target_os = "linux")]
{
Err(anyhow::anyhow!(
"WavReader is only supported on non-Linux platforms"
))
}
}
pub fn read_one_frame(&mut self) -> Result<Option<&[i16]>> {
#[cfg(not(target_os = "linux"))]
{
match self.reader.read_chunk(self.config.frame_size)? {
Some((left_pcm, right_pcm)) => {
let actual_len = left_pcm.len();
if self.config.channels == 2 {
for i in 0..actual_len {
self.pcm_buffer[i * 2] = left_pcm[i];
self.pcm_buffer[i * 2 + 1] = right_pcm[i];
}
} else {
self.pcm_buffer[..actual_len].copy_from_slice(&left_pcm);
}
let total_len = actual_len * (self.config.channels as usize);
Ok(Some(&self.pcm_buffer[..total_len]))
}
None => Ok(None),
}
}
#[cfg(target_os = "linux")]
{
Err(anyhow::anyhow!(
"WavReader is only supported on non-Linux platforms"
))
}
}
}
+99
View File
@@ -0,0 +1,99 @@
//! # Client Demo
//!
//! 演示客户端的主要功能:
//! - 自动服务发现
//! - 响应 RPC 调用
//! - 音频录制和播放
//! - 事件处理
use std::sync::Arc;
use xiao::app::client::{Client, ClientConfig};
use xiao::net::command::Command;
use xiao::net::event::EventData;
use xiao::net::rpc::RpcBuilder;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
println!("╔═══════════════════════════════════════════════════════╗");
println!(
"║ XiaoAi Audio Client v{}",
env!("CARGO_PKG_VERSION")
);
println!("╚═══════════════════════════════════════════════════════╝");
println!();
// 创建客户端
let config = ClientConfig::default();
let client = Arc::new(Client::new(config));
// 启动事件监听器
let event_client = client.clone();
tokio::spawn(async move {
let mut rx = event_client.subscribe_events();
while let Some((event, ts, addr)) = rx.recv().await {
match event {
EventData::Hello { message, .. } => {
println!("[Event] Hello: {} ts:{} addr:{}", message, ts, addr);
}
_ => {}
}
}
});
// 启动客户端主循环
let run_client = client.clone();
tokio::spawn(async move {
if let Err(e) = run_client.run().await {
eprintln!("Client error: {}", e);
}
});
println!("Client is running, searching for server...\n");
// 等待连接
loop {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
if client.is_connected().await {
break;
}
}
println!("\n═══════════════════════════════════════════════════════");
println!("Connected to server!");
println!("Running client-side tests...\n");
// 1. 测试简单的 Shell 命令
println!("1️⃣ Testing Shell command...");
match client
.rpc(&RpcBuilder::default(Command::Shell(
"echo 'Hello from client'".to_string(),
)))
.await
{
Ok(resp) => println!(" ✅ Command executed: {:?}", resp),
Err(e) => println!(" ❌ Command failed: {}", e),
}
// 2. 发送客户端事件
println!("\n2️⃣ Sending event to server...");
match client
.send_event(EventData::Hello {
message: "from client!".to_string(),
})
.await
{
Ok(_) => println!(" ✅ Event sent"),
Err(e) => println!(" ❌ Failed to send event: {}", e),
}
println!("\n═══════════════════════════════════════════════════════");
println!("✅ Client tests completed!");
println!("\nClient is now ready to receive commands from server.");
println!("Press Ctrl+C to exit.\n");
// 保持运行,等待服务器命令
tokio::signal::ctrl_c().await?;
println!("\nShutting down client...");
client.shutdown();
Ok(())
}
+153
View File
@@ -0,0 +1,153 @@
//! # Server Demo
//!
//! 演示服务端的主要功能:
//! - 多客户端管理
//! - RPC 调用
//! - 音频录制和播放
//! - 事件广播
use std::sync::Arc;
use xiao::app::server::{Server, ServerConfig};
use xiao::audio::config::AudioConfig;
use xiao::net::command::Command;
use xiao::net::event::EventData;
use xiao::net::rpc::RpcBuilder;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
println!("╔═══════════════════════════════════════════════════════╗");
println!(
"║ XiaoAi Audio Server v{}",
env!("CARGO_PKG_VERSION")
);
println!("╚═══════════════════════════════════════════════════════╝");
println!();
// 创建服务端
let config = ServerConfig::default();
let server = Arc::new(Server::new(config).await?);
let s = server.clone();
// 启动服务器
tokio::spawn(async move {
if let Err(e) = s.run(8080).await {
eprintln!("Server error: {}", e);
}
});
// 启动事件监听器
let event_server = server.clone();
tokio::spawn(async move {
let mut rx = event_server.subscribe_events();
while let Some((event, ts, addr)) = rx.recv().await {
match event {
EventData::Hello { message, .. } => {
println!("[Event] Hello: {} ts:{} addr:{}", message, ts, addr);
}
_ => {}
}
}
});
println!("Server is running on port 8080");
println!("Waiting for clients to connect...\n");
// 主循环:等待客户端并执行测试
loop {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let clients = server.get_clients().await;
if clients.is_empty() {
continue;
}
let addr = clients[0];
println!("\n═══════════════════════════════════════════════════════");
println!("Client connected: {}", addr);
println!("Running demo tests...\n");
// 1. 测试 Shell 命令(同步)
println!("1️⃣ Testing Shell RPC (sync)...");
match server
.rpc(
addr,
&RpcBuilder::default(Command::Shell("echo 'Hello from server'".to_string())),
)
.await
{
Ok(resp) => println!(" ✅ Command executed: {:?}", resp),
Err(e) => println!(" ❌ Command failed: {}", e),
}
// 2. 测试 Shell 命令(带超时)
println!("\n2️⃣ Testing Shell RPC with timeout (1s)...");
match server
.rpc(
addr,
&RpcBuilder::default(Command::Shell("sleep 2".to_string())).set_timeout(1000),
)
.await
{
Ok(resp) => println!(" ✅ Command executed: {:?}", resp),
Err(e) => println!(" ❌ Command failed: {}", e),
}
// 3. 测试事件
println!("\n3️⃣ Broadcasting notification event...");
match server
.send_event(
addr,
EventData::Hello {
message: "from server!".to_string(),
},
)
.await
{
Ok(_) => println!(" ✅ Event sent"),
Err(e) => println!(" ❌ Failed to send event: {}", e),
}
// 5. 测试音频录制
println!("\n5️⃣ Testing Audio Recording (10 seconds)...");
match server.start_record(addr, AudioConfig::voice_16k()).await {
Ok(_) => {
println!(" ⏺️ Recording started...");
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
server.stop_record(addr).await?;
println!(" ⏹️ Recording stopped");
}
Err(e) => println!(" ❌ Recording failed: {}", e),
}
// 6. 测试音频播放(如果有测试文件)
println!("\n6️⃣ Testing Audio Playback...");
if std::path::Path::new("temp/test.wav").exists() {
match server.start_play(addr, "temp/test.wav").await {
Ok(_) => {
println!(" ▶️ Playback started...");
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
server.stop_play(addr).await?;
println!(" ⏹️ Playback stopped");
}
Err(e) => println!(" ❌ Playback failed: {}", e),
}
} else {
println!(" ⚠️ No test file found at temp/test.wav, skipping...");
}
println!("\n═══════════════════════════════════════════════════════");
println!("✅ All tests completed!");
println!("\nServer status:");
println!(" • Connected clients: {}", server.client_count());
println!(" • Uptime: {} seconds", server.uptime_secs());
println!("\nPress Ctrl+C to exit.");
break;
}
// 等待退出信号
tokio::signal::ctrl_c().await?;
println!("\nShutting down server...");
server.shutdown();
Ok(())
}
+4
View File
@@ -0,0 +1,4 @@
pub mod app;
pub mod audio;
pub mod net;
pub mod utils;
+121
View File
@@ -0,0 +1,121 @@
//! # Command - RPC 命令类型系统
//!
//! 支持多种类型的命令,每种命令有独立的请求和响应结构。
//!
//! ## 设计
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────────┐
//! │ Command Types │
//! │ │
//! │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
//! │ │ Shell │ │ GetInfo │ │ SetVolume │ ... │
//! │ │ cmd → out │ │ () → Info │ │ vol → () │ │
//! │ └──────────────┘ └──────────────┘ └──────────────┘ │
//! │ │
//! │ ┌──────────────────┐ │
//! │ │ RpcRequest │ │
//! │ │ id + Command │ │
//! │ └────────┬─────────┘ │
//! │ │ │
//! │ ▼ │
//! │ ┌──────────────────┐ │
//! │ │ RpcResponse │ │
//! │ │ id + Result │ │
//! │ └──────────────────┘ │
//! └─────────────────────────────────────────────────────────────┘
//! ```
use serde::{Deserialize, Serialize};
use crate::utils::shell::{ShellRequest, ShellResponse};
// ==================== 命令请求类型 ====================
// ==================== 统一命令枚举 ====================
/// RPC 命令 - 统一的请求类型
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum Command {
/// 执行 Shell 命令
Shell(ShellRequest),
}
/// RPC 结果 - 统一的响应类型
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum CommandResult {
/// 错误
Error(CommandError),
/// Shell 命令结果
Shell(ShellResponse),
}
/// 命令错误
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CommandError {
pub code: i32,
pub message: String,
}
impl CommandError {
pub fn new(code: i32, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
}
}
pub fn not_found(msg: impl Into<String>) -> Self {
Self::new(-1, msg)
}
pub fn invalid_args(msg: impl Into<String>) -> Self {
Self::new(-2, msg)
}
pub fn permission_denied(msg: impl Into<String>) -> Self {
Self::new(-3, msg)
}
pub fn internal(msg: impl Into<String>) -> Self {
Self::new(-500, msg)
}
pub fn timeout(msg: impl Into<String>) -> Self {
Self::new(-408, msg)
}
pub fn not_implemented() -> Self {
Self::new(-501, "Not implemented")
}
}
impl std::fmt::Display for CommandError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.code, self.message)
}
}
impl std::error::Error for CommandError {}
// ==================== 便捷构造方法 ====================
impl CommandResult {
/// 创建错误响应
pub fn error(err: CommandError) -> Self {
Self::Error(err)
}
/// 检查是否是错误
pub fn is_error(&self) -> bool {
matches!(self, Self::Error(_))
}
/// 获取错误(如果有)
pub fn as_error(&self) -> Option<&CommandError> {
match self {
Self::Error(e) => Some(e),
_ => None,
}
}
}
+46
View File
@@ -0,0 +1,46 @@
use crate::net::protocol::ControlPacket;
use anyhow::Result;
use std::net::{IpAddr, SocketAddr};
use std::time::Duration;
use tokio::net::UdpSocket;
const DISCOVERY_PROTOCOL: &str = "XIAO_V2";
pub const DISCOVERY_PORT: u16 = 53530;
pub struct Discovery;
impl Discovery {
pub async fn broadcast(port: u16) -> Result<()> {
let socket = UdpSocket::bind("0.0.0.0:0").await?;
socket.set_broadcast(true)?;
let target: SocketAddr = format!("255.255.255.255:{}", DISCOVERY_PORT).parse()?;
let msg = postcard::to_allocvec(&ControlPacket::Discovery {
protocol: DISCOVERY_PROTOCOL.to_string(),
port,
})?;
tokio::spawn(async move {
loop {
let _ = socket.send_to(&msg, target).await;
tokio::time::sleep(Duration::from_secs(1)).await;
}
});
Ok(())
}
pub async fn listen() -> Result<(IpAddr, u16)> {
let socket = UdpSocket::bind(format!("0.0.0.0:{}", DISCOVERY_PORT)).await?;
let mut buf = [0u8; 1024];
loop {
let (len, addr) = socket.recv_from(&mut buf).await?;
let data = &buf[..len];
if let Ok(ControlPacket::Discovery { protocol, port }) = postcard::from_bytes(data) {
if protocol == DISCOVERY_PROTOCOL {
return Ok((addr.ip(), port));
}
}
}
}
}
+95
View File
@@ -0,0 +1,95 @@
//! # Event - 实时事件系统
//!
//! 支持双向的实时事件推送,包括:
//! - 服务端事件(推送给客户端)
//! - 客户端事件(推送给服务端)
//! - 事件订阅和过滤
//!
//! ## 设计
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────────┐
//! │ Event System │
//! │ │
//! │ Server Events: │
//! │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
//! │ │ AudioStatus │ │ ClientJoined │ │ Message │ ... │
//! │ └──────────────┘ └──────────────┘ └──────────────┘ │
//! │ │
//! │ Client Events: │
//! │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
//! │ │ StatusUpdate │ │ Metrics │ │ Alert │ ... │
//! │ └──────────────┘ └──────────────┘ └──────────────┘ │
//! │ │
//! │ ┌─────────────────────┐ │
//! │ │ EventBus │ │
//! │ │ broadcast channel │ │
//! │ └─────────────────────┘ │
//! └─────────────────────────────────────────────────────────────┘
//! ```
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use tokio::sync::broadcast;
// ==================== 事件类型 ====================
/// 客户端事件(Client → Server
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum EventData {
/// 测试事件
Hello { message: String },
}
// ==================== 事件总线 ====================
/// 事件订阅者信息
pub struct EventSubscription<E> {
pub receiver: broadcast::Receiver<E>,
}
impl<E: Clone> EventSubscription<E> {
/// 接收下一个事件
pub async fn recv(&mut self) -> Option<E> {
match self.receiver.recv().await {
Ok(event) => Some(event),
Err(broadcast::error::RecvError::Lagged(_)) => {
// 跳过丢失的事件,继续接收
Box::pin(self.recv()).await
}
Err(broadcast::error::RecvError::Closed) => None,
}
}
}
/// 事件总线(单向,只接收转发)
pub struct EventBus {
tx: broadcast::Sender<(EventData, u128, SocketAddr)>,
}
pub type EventBusSubscription = EventSubscription<(EventData, u128, SocketAddr)>;
impl EventBus {
pub fn new(capacity: usize) -> Self {
let (tx, _) = broadcast::channel(capacity);
Self { tx }
}
/// 收到事件
pub fn receive(&self, event: EventData, timestamp: u128, addr: SocketAddr) {
let _ = self.tx.send((event, timestamp, addr));
}
/// 订阅事件
pub fn subscribe(&self) -> EventBusSubscription {
EventSubscription {
receiver: self.tx.subscribe(),
}
}
}
impl Default for EventBus {
fn default() -> Self {
Self::new(128)
}
}
+334
View File
@@ -0,0 +1,334 @@
//! # Jitter Buffer - 抖动缓冲区
//!
//! 用于音频流的抖动缓冲和包重排序。
//!
//! ## 功能
//! - 自适应缓冲区大小
//! - 乱序包重排
//! - 丢包检测和统计
//! - 延迟统计
use crate::net::protocol::AudioPacket;
use std::cmp::Ordering;
use std::collections::{BTreeMap, VecDeque};
impl PartialEq for AudioPacket {
fn eq(&self, other: &Self) -> bool {
self.timestamp == other.timestamp
}
}
impl Eq for AudioPacket {}
impl Ord for AudioPacket {
fn cmp(&self, other: &Self) -> Ordering {
other.timestamp.cmp(&self.timestamp)
}
}
impl PartialOrd for AudioPacket {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
/// Jitter Buffer 统计信息
#[derive(Debug, Clone, Default)]
pub struct JitterStats {
/// 总接收包数
pub received: u64,
/// 总丢包数
pub lost: u64,
/// 总播放包数
pub played: u64,
/// 迟到的包数(到达时已过播放时间)
pub late: u64,
/// 重复包数
pub duplicate: u64,
/// 当前缓冲区大小(包数)
pub buffer_size: usize,
/// 最小延迟(微秒)
pub min_delay: u128,
/// 最大延迟(微秒)
pub max_delay: u128,
/// 平均延迟(微秒)
pub avg_delay: u128,
}
impl JitterStats {
/// 计算丢包率(百分比)
pub fn loss_rate(&self) -> f64 {
if self.received + self.lost == 0 {
return 0.0;
}
(self.lost as f64 / (self.received + self.lost) as f64) * 100.0
}
}
/// Jitter Buffer 配置
#[derive(Debug, Clone)]
pub struct JitterConfig {
/// 最小缓冲区大小(包数)
pub min_buffer_size: usize,
/// 最大缓冲区大小(包数)
pub max_buffer_size: usize,
/// 目标缓冲区大小(包数)
pub target_buffer_size: usize,
/// 自适应调整间隔(包数)
pub adapt_interval: usize,
/// 最大容忍延迟(微秒)
pub max_tolerable_delay: u128,
}
impl Default for JitterConfig {
fn default() -> Self {
Self {
min_buffer_size: 2,
max_buffer_size: 20,
target_buffer_size: 5,
adapt_interval: 50,
max_tolerable_delay: 100_000, // 100ms
}
}
}
/// Jitter Buffer - 用于音频流的抖动缓冲
pub struct JitterBuffer {
/// 配置
config: JitterConfig,
/// 缓冲区(按时间戳排序)
buffer: BTreeMap<u128, AudioPacket>,
/// 统计信息
stats: JitterStats,
/// 期望的下一个序列号
expected_seq: u32,
/// 是否已接收第一个包
first_packet_received: bool,
/// 延迟样本窗口(用于自适应)
delay_samples: VecDeque<u128>,
/// 自适应计数器
adapt_counter: usize,
/// 上次播放的时间戳
last_played_timestamp: u128,
}
impl JitterBuffer {
/// 创建新的 Jitter Buffer
pub fn new(config: JitterConfig) -> Self {
Self {
config,
buffer: BTreeMap::new(),
stats: JitterStats::default(),
expected_seq: 0,
first_packet_received: false,
delay_samples: VecDeque::with_capacity(100),
adapt_counter: 0,
last_played_timestamp: 0,
}
}
/// 使用默认配置创建
pub fn default() -> Self {
Self::new(JitterConfig::default())
}
/// 插入音频包
pub fn push(&mut self, packet: AudioPacket, arrival_time: u128) {
// 检测重复包
if self.buffer.contains_key(&packet.timestamp) {
self.stats.duplicate += 1;
return;
}
// 初始化序列号
if !self.first_packet_received {
self.expected_seq = packet.seq.wrapping_add(1);
self.first_packet_received = true;
} else {
// 检测丢包
let seq_diff = packet.seq.wrapping_sub(self.expected_seq);
if seq_diff > 0 && seq_diff < 1000 {
// 允许一定的序列号跳跃(处理回环)
self.stats.lost += seq_diff as u64;
}
self.expected_seq = packet.seq.wrapping_add(1);
}
// 检查是否迟到
if self.last_played_timestamp > 0 && packet.timestamp < self.last_played_timestamp {
self.stats.late += 1;
return;
}
// 超过最大容忍延迟,直接丢弃
if packet.timestamp < arrival_time
&& arrival_time - packet.timestamp > self.config.max_tolerable_delay
{
self.stats.late += 1;
return;
}
let delay = if packet.timestamp >= arrival_time {
packet.timestamp - arrival_time
} else {
0 // 包到达时已经过了播放时间
};
self.update_delay_stats(delay);
// 插入缓冲区
self.buffer.insert(packet.timestamp, packet);
self.stats.received += 1;
self.stats.buffer_size = self.buffer.len();
// 自适应调整
self.adapt_counter += 1;
if self.adapt_counter >= self.config.adapt_interval {
self.adapt_buffer_size();
self.adapt_counter = 0;
}
}
/// 获取下一个应该播放的包
///
/// # Arguments
/// * `current_time` - 当前服务器时间
///
/// # Returns
/// * `Some(packet)` - 如果有包应该播放
/// * `None` - 如果缓冲区为空或还没到播放时间
pub fn pop(&mut self, current_time: u128) -> Option<AudioPacket> {
// 如果缓冲区为空,直接返回
if self.buffer.is_empty() {
return None;
}
// 检查缓冲区是否达到最小大小
if self.buffer.len() < self.config.target_buffer_size && !self.should_drain() {
return None;
}
// 获取最早的包
if let Some((&timestamp, _)) = self.buffer.iter().next() {
// 检查是否到达播放时间
if current_time >= timestamp {
let packet = self.buffer.remove(&timestamp).unwrap();
self.last_played_timestamp = timestamp;
self.stats.played += 1;
self.stats.buffer_size = self.buffer.len();
return Some(packet);
}
}
None
}
/// 强制获取下一个包(无论时间)
pub fn pop_next(&mut self) -> Option<AudioPacket> {
if let Some((&timestamp, _)) = self.buffer.iter().next() {
let packet = self.buffer.remove(&timestamp).unwrap();
self.last_played_timestamp = timestamp;
self.stats.played += 1;
self.stats.buffer_size = self.buffer.len();
return Some(packet);
}
None
}
/// 查看下一个包的播放时间(不移除)
pub fn peek_next_timestamp(&self) -> Option<u128> {
self.buffer.keys().next().copied()
}
/// 获取缓冲区大小
pub fn len(&self) -> usize {
self.buffer.len()
}
/// 检查缓冲区是否为空
pub fn is_empty(&self) -> bool {
self.buffer.is_empty()
}
/// 获取统计信息
pub fn stats(&self) -> &JitterStats {
&self.stats
}
/// 重置统计信息
pub fn reset_stats(&mut self) {
self.stats = JitterStats::default();
self.stats.buffer_size = self.buffer.len();
}
/// 清空缓冲区
pub fn clear(&mut self) {
self.buffer.clear();
self.stats.buffer_size = 0;
self.first_packet_received = false;
self.last_played_timestamp = 0;
}
/// 检查是否应该排空缓冲区(处理长时间没有新包的情况)
fn should_drain(&self) -> bool {
// 如果缓冲区有包且已经等了很久,就开始播放
!self.buffer.is_empty() && self.buffer.len() >= self.config.min_buffer_size
}
/// 更新延迟统计
fn update_delay_stats(&mut self, delay: u128) {
// 更新最小/最大延迟
if self.stats.received == 0 {
self.stats.min_delay = delay;
self.stats.max_delay = delay;
self.stats.avg_delay = delay;
} else {
self.stats.min_delay = self.stats.min_delay.min(delay);
self.stats.max_delay = self.stats.max_delay.max(delay);
// 滑动平均
self.stats.avg_delay = (self.stats.avg_delay * 9 + delay) / 10;
}
// 保存延迟样本用于自适应
self.delay_samples.push_back(delay);
if self.delay_samples.len() > 100 {
self.delay_samples.pop_front();
}
}
/// 自适应调整缓冲区大小
fn adapt_buffer_size(&mut self) {
if self.delay_samples.len() < 10 {
return;
}
// 计算延迟方差(抖动)
let avg = self.stats.avg_delay;
let variance: f64 = self
.delay_samples
.iter()
.map(|&d| {
let diff = d as i128 - avg as i128;
(diff * diff) as f64
})
.sum::<f64>()
/ self.delay_samples.len() as f64;
let jitter = variance.sqrt();
// 根据抖动调整目标缓冲区大小
// 抖动大 -> 增加缓冲区
// 抖动小 -> 减少缓冲区
let target = if jitter > 50_000.0 {
// 高抖动(>50ms 标准差)
self.config.target_buffer_size + 2
} else if jitter < 10_000.0 {
// 低抖动(<10ms 标准差)
self.config.target_buffer_size.saturating_sub(1)
} else {
self.config.target_buffer_size
};
// 限制在最小/最大范围内
self.config.target_buffer_size = target
.max(self.config.min_buffer_size)
.min(self.config.max_buffer_size);
}
}
+20
View File
@@ -0,0 +1,20 @@
//! # Net 模块
//!
//! 网络通信相关模块:
//! - `command` - RPC 命令类型系统
//! - `discovery` - 服务发现
//! - `event` - 实时事件系统
//! - `network` - 底层网络连接
//! - `protocol` - 通信协议定义
//! - `rpc` - RPC 调用管理
//! - `sync` - 时间同步工具
//! - `jitter_buffer` - 抖动缓冲区实现
pub mod command;
pub mod discovery;
pub mod event;
pub mod jitter_buffer;
pub mod network;
pub mod protocol;
pub mod rpc;
pub mod sync;
+81
View File
@@ -0,0 +1,81 @@
use crate::net::protocol::{AudioPacket, ControlPacket};
use anyhow::Result;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpStream, UdpSocket};
use tokio::sync::Mutex;
/// A unified control connection over TCP
pub struct Connection {
reader: Mutex<tokio::net::tcp::OwnedReadHalf>,
writer: Mutex<tokio::net::tcp::OwnedWriteHalf>,
peer_addr: SocketAddr,
}
impl Connection {
pub fn new(stream: TcpStream) -> Result<Self> {
let peer_addr = stream.peer_addr()?;
let (r, w) = stream.into_split();
Ok(Self {
reader: Mutex::new(r),
writer: Mutex::new(w),
peer_addr,
})
}
pub async fn send(&self, packet: &ControlPacket) -> Result<()> {
let bytes = postcard::to_allocvec(packet)?;
let mut writer = self.writer.lock().await;
writer.write_u32(bytes.len() as u32).await?;
writer.write_all(&bytes).await?;
writer.flush().await?;
Ok(())
}
pub async fn recv(&self) -> Result<ControlPacket> {
let mut reader = self.reader.lock().await;
let len = reader.read_u32().await? as usize;
if len > 1024 * 1024 {
return Err(anyhow::anyhow!("Packet too large: {}", len));
}
let mut buf = vec![0u8; len];
reader.read_exact(&mut buf).await?;
let packet = postcard::from_bytes(&buf)?;
Ok(packet)
}
pub fn peer_addr(&self) -> SocketAddr {
self.peer_addr
}
}
/// UDP Socket for audio transmission
pub struct AudioSocket {
socket: Arc<UdpSocket>,
}
impl AudioSocket {
pub async fn bind() -> Result<Self> {
let socket = UdpSocket::bind("0.0.0.0:0").await?;
Ok(Self {
socket: Arc::new(socket),
})
}
pub fn port(&self) -> u16 {
self.socket.local_addr().unwrap().port()
}
pub async fn send(&self, packet: &AudioPacket, target: SocketAddr) -> Result<()> {
let bytes = postcard::to_allocvec(packet)?;
self.socket.send_to(&bytes, target).await?;
Ok(())
}
pub async fn recv(&self, buf: &mut [u8]) -> Result<(AudioPacket, SocketAddr)> {
let (len, addr) = self.socket.recv_from(buf).await?;
let packet = postcard::from_bytes(&buf[..len])?;
Ok((packet, addr))
}
}
+100
View File
@@ -0,0 +1,100 @@
//! # Protocol - 通信协议定义
//!
//! 定义 Client 和 Server 之间的所有通信协议。
use crate::audio::config::AudioConfig;
use crate::net::command::{Command, CommandResult};
use crate::net::event::EventData;
use serde::{Deserialize, Serialize};
// ==================== 基础类型 ====================
/// 客户端信息
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ClientInfo {
pub model: String,
pub serial_number: String,
}
/// 音频数据包 - UDP 通道传输
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AudioPacket {
pub seq: u32, // 序列号,用于丢包检测
pub timestamp: u128, // 目标播放时间 (主节点时间)
pub data: Vec<u8>, // Opus 编码数据
}
// ==================== 控制包 ====================
/// 控制包 - TCP 通道传输的所有消息类型
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum ControlPacket {
// ========== 服务发现 ==========
/// 服务发现广播
Discovery {
protocol: String,
port: u16,
},
// ========== 握手 ==========
/// 服务端握手
ServerHello {
auth: String,
version: String,
udp_port: u16,
},
/// 客户端握手
ClientHello {
auth: String,
version: String,
udp_port: u16,
info: ClientInfo,
},
// ========== 心跳 ==========
Ping {
client_ts: u128,
seq: u32,
},
Pong {
client_ts: u128,
server_ts: u128,
seq: u32,
},
// ========== RPC ==========
RpcRequest {
id: u32,
/// 是否异步运行
run_async: bool,
/// 超时(毫秒)
timeout: Option<u64>,
/// 请求命令
command: Command,
},
RpcResponse {
id: u32,
result: CommandResult,
},
// ========== 事件 ==========
Event {
/// 微秒时间戳
timestamp: u128,
data: EventData,
},
// ========== 音频控制 ==========
/// 开始录音
StartRecording {
config: AudioConfig,
},
/// 停止录音
StopRecording,
/// 开始播放
StartPlayback {
config: AudioConfig,
},
/// 停止播放
StopPlayback,
}
+229
View File
@@ -0,0 +1,229 @@
//! # RPC Manager - RPC 调用管理
//!
//! 管理 RPC 请求的生命周期,包括:
//! - 请求 ID 生成
//! - 请求/响应匹配
//! - 超时处理
//! - 异步任务管理
use crate::net::command::{Command, CommandError, CommandResult};
use crate::net::protocol::ControlPacket;
use crate::utils::shell::Shell;
use anyhow::Result;
use dashmap::DashMap;
use std::future::Future;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use thiserror::Error;
use tokio::sync::oneshot;
#[derive(Debug, Error)]
pub enum RpcError {
#[error("RPC timeout")]
Timeout,
#[error("RPC cancelled")]
Cancelled,
#[error("RPC internal error: {0}")]
Internal(String),
}
pub struct RpcManager {
handler: RpcHandler,
next_id: AtomicU32,
pending: Arc<DashMap<u32, oneshot::Sender<CommandResult>>>,
}
impl RpcManager {
pub fn new() -> Self {
Self {
handler: RpcHandler::new(),
next_id: AtomicU32::new(1),
pending: Arc::new(DashMap::new()),
}
}
pub fn resolve(&self, id: u32, result: CommandResult) {
if let Some((_, tx)) = self.pending.remove(&id) {
let _ = tx.send(result);
}
}
pub fn cancel(&self, id: u32) {
self.pending.remove(&id);
}
/// 发起异步 RPC 调用
///
/// F: 异步闭包,输入 ID,返回 Future
pub async fn call<F, Fut>(
&self,
builder: &RpcBuilder,
send_fn: F,
) -> Result<CommandResult, RpcError>
where
F: FnOnce(ControlPacket) -> Fut,
Fut: Future<Output = Result<()>>,
{
let command = builder.command.clone();
let timeout = builder.timeout;
let run_async = builder.run_async;
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
let (tx, rx) = oneshot::channel();
// 1. 注册请求
self.pending.insert(id, tx);
// 2. RAII 守卫:确保无论因超时、报错还是外部 Drop,ID 都会被清理
let _guard = DropGuard {
id,
pending: Arc::clone(&self.pending),
};
// 3. 执行异步发送逻辑
if let Err(e) = send_fn(ControlPacket::RpcRequest {
id,
command,
timeout,
run_async,
})
.await
{
return Err(RpcError::Internal(e.to_string()));
}
// 4. 等待响应或超时
match timeout {
Some(ms) => match tokio::time::timeout(Duration::from_millis(ms), rx).await {
Ok(Ok(res)) => Ok(res),
Ok(Err(_)) => Err(RpcError::Cancelled), // tx 关闭,任务被取消
Err(_) => Err(RpcError::Timeout),
},
None => rx.await.map_err(|_| RpcError::Cancelled), // tx 关闭,任务被取消
}
}
/// 处理 RPC 调用
///
/// F: 异步闭包,输入 ID,返回 Future
pub async fn handle_rpc_request<F, Fut>(
&self,
id: u32,
run_async: bool,
timeout_ms: Option<u64>,
command: Command,
send_fn: F,
) -> Result<()>
where
F: Fn(ControlPacket) -> Fut + Send + Sync + 'static, // 异步执行需要 'static
Fut: Future<Output = Result<()>> + Send,
{
let sender = Arc::new(send_fn);
let handler = self.handler.clone();
let task = async move {
let execute_future = handler.handle(command, timeout_ms);
match timeout_ms {
None => {
// 无超时逻辑
let result = execute_future.await;
sender(ControlPacket::RpcResponse { id, result }).await
}
Some(ms) => {
let d = Duration::from_millis(ms);
match tokio::time::timeout(d, execute_future).await {
Ok(result) => {
// 1. 正常完成
sender(ControlPacket::RpcResponse { id, result }).await
}
Err(_) => {
// 2. 触发超时:发送超时错误消息
let err_result = CommandResult::error(CommandError::timeout(format!(
"Task {} timed out after {}ms",
id, ms
)));
sender(ControlPacket::RpcResponse {
id,
result: err_result,
})
.await
}
}
}
}
};
if run_async {
tokio::spawn(task);
Ok(())
} else {
task.await
}
}
}
#[derive(Debug, Clone)]
pub struct RpcBuilder {
pub command: Command,
pub run_async: bool,
pub timeout: Option<u64>,
}
impl RpcBuilder {
/// 基础构造器
pub fn new(command: Command) -> Self {
Self {
command,
run_async: false,
timeout: None,
}
}
/// 便捷方法:默认异步配置
pub fn default(command: Command) -> Self {
Self::new(command).set_async(true).set_timeout(10_000)
}
/// 设置为异步运行
pub fn set_async(mut self, is_async: bool) -> Self {
self.run_async = is_async;
self
}
/// 设置超时时长(毫秒)
pub fn set_timeout(mut self, ms: u64) -> Self {
self.timeout = Some(ms);
self
}
}
#[derive(Debug, Clone)]
pub struct RpcHandler;
impl RpcHandler {
fn new() -> Self {
Self {}
}
async fn handle(&self, command: Command, timeout_ms: Option<u64>) -> CommandResult {
match command {
Command::Shell(req) => match Shell::run(req, timeout_ms).await {
Ok(resp) => CommandResult::Shell(resp),
Err(e) => CommandResult::Error(CommandError::internal(e.to_string())),
},
_ => CommandResult::Error(CommandError::not_implemented()),
}
}
}
struct DropGuard {
id: u32,
pending: Arc<DashMap<u32, oneshot::Sender<CommandResult>>>,
}
impl Drop for DropGuard {
fn drop(&mut self) {
self.pending.remove(&self.id);
}
}
+222
View File
@@ -0,0 +1,222 @@
use std::collections::VecDeque;
use std::time::{SystemTime, UNIX_EPOCH};
/// 获取当前微秒级时间戳
pub fn now_us() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("时间倒流")
.as_micros()
}
/// 时钟同步管理器,用于计算主从节点间的时钟偏移
/// 采用改进的 NTP 算法 + Kalman 滤波思想
pub struct ClockSync {
/// 偏移量样本窗口
offsets: VecDeque<OffsetSample>,
/// 当前估计的时钟偏移 (server_time - client_time)
pub current_offset: i128,
/// RTT 样本窗口
rtts: VecDeque<i128>,
/// 当前估计的最小 RTT
min_rtt: i128,
/// 窗口大小
window_size: usize,
/// 时钟漂移率 (ppm: parts per million)
/// 正值表示从节点时钟比主节点快
drift_rate: f64,
/// 上次更新时间
last_update_time: u128,
/// 漂移率估计窗口
drift_samples: VecDeque<DriftSample>,
}
#[derive(Clone, Copy)]
struct OffsetSample {
offset: i128,
rtt: i128,
}
#[derive(Clone, Copy)]
struct DriftSample {
offset: i128,
timestamp: u128,
}
impl ClockSync {
pub fn new(window_size: usize) -> Self {
Self {
offsets: VecDeque::with_capacity(window_size),
current_offset: 0,
rtts: VecDeque::with_capacity(window_size),
min_rtt: i128::MAX,
window_size,
drift_rate: 0.0,
last_update_time: now_us(),
drift_samples: VecDeque::with_capacity(60), // 保留 60 秒的样本
}
}
/// 更新时钟偏移估计 (NTP 算法)
///
/// NTP 时间戳标记:
/// t1 = client_send_ts : 客户端发送 Ping 的时间
/// t2 = server_ts : 服务器接收 Ping 的时间
/// t3 = server_ts : 服务器发送 Pong 的时间 (假设处理时间忽略不计)
/// t4 = client_recv_ts : 客户端接收 Pong 的时间
///
/// RTT = (t4 - t1) - (t3 - t2) = (t4 - t1) (因为 t3 = t2)
/// Offset = ((t2 - t1) + (t3 - t4)) / 2 = ((t2 - t1) + (t2 - t4)) / 2
/// = t2 - (t1 + t4) / 2
pub fn update(&mut self, client_send_ts: u128, server_ts: u128, client_recv_ts: u128) {
let t1 = client_send_ts as i128;
let t2 = server_ts as i128;
let t4 = client_recv_ts as i128;
let rtt = t4 - t1;
// 过滤异常 RTT (局域网内 > 100ms 视为异常)
if rtt < 0 || rtt > 100_000 {
return;
}
// 计算时钟偏移: offset = server_time - client_time
// offset = t2 - (t1 + t4) / 2
let offset = t2 - (t1 + t4) / 2;
// 更新 RTT 窗口
self.rtts.push_back(rtt);
if self.rtts.len() > self.window_size {
self.rtts.pop_front();
}
self.min_rtt = *self.rtts.iter().min().unwrap_or(&rtt);
// 更新偏移量窗口
let sample = OffsetSample {
offset,
rtt,
};
self.offsets.push_back(sample);
if self.offsets.len() > self.window_size {
self.offsets.pop_front();
}
// 偏移量估计: 使用低 RTT 样本的中位数
// 原理: RTT 较小的样本受网络抖动影响小,时间测量更准确
let mut low_rtt_offsets: Vec<i128> = self
.offsets
.iter()
.filter(|s| s.rtt <= self.min_rtt + 5000) // 5ms 容差
.map(|s| s.offset)
.collect();
if !low_rtt_offsets.is_empty() {
low_rtt_offsets.sort_unstable();
let new_offset = low_rtt_offsets[low_rtt_offsets.len() / 2];
// 漂移率估计
self.estimate_drift(new_offset, client_recv_ts);
// 平滑更新偏移量 (避免突变)
let alpha = 0.3; // 低通滤波系数
self.current_offset =
(alpha * new_offset as f64 + (1.0 - alpha) * self.current_offset as f64) as i128;
}
self.last_update_time = client_recv_ts;
}
/// 估计时钟漂移率
/// 时钟漂移率 = d(offset) / dt
fn estimate_drift(&mut self, offset: i128, timestamp: u128) {
self.drift_samples
.push_back(DriftSample { offset, timestamp });
if self.drift_samples.len() > 60 {
self.drift_samples.pop_front();
}
// 至少需要 10 秒的数据才能估计漂移
if self.drift_samples.len() < 10 {
return;
}
// 使用线性回归估计漂移率
let first = self.drift_samples.front().unwrap();
let last = self.drift_samples.back().unwrap();
let dt = (last.timestamp - first.timestamp) as f64;
let d_offset = (last.offset - first.offset) as f64;
if dt > 10_000_000.0 {
// 超过 10 秒
// drift_rate 单位: 微秒/秒 = ppm
let new_drift = d_offset / (dt / 1_000_000.0);
// 平滑更新漂移率
let beta = 0.1;
self.drift_rate = beta * new_drift + (1.0 - beta) * self.drift_rate;
}
}
/// 将本地时间转换为服务器(主节点)时间
/// 考虑时钟漂移补偿
pub fn to_server_time(&self, client_time: u128) -> u128 {
let base_server_time = (client_time as i128 + self.current_offset) as u128;
// 漂移补偿: 根据距离上次同步的时间,补偿时钟漂移
let elapsed_since_update = client_time.saturating_sub(self.last_update_time) as f64;
let drift_correction = (self.drift_rate * elapsed_since_update / 1_000_000.0) as i128;
(base_server_time as i128 + drift_correction) as u128
}
/// 将服务器(主节点)时间转换为本地时间
pub fn to_client_time(&self, server_time: u128) -> u128 {
// 简化版本,不考虑漂移补偿 (播放时主要用 to_server_time)
(server_time as i128 - self.current_offset) as u128
}
/// 获取当前估计的 RTT (微秒)
pub fn get_rtt(&self) -> i128 {
self.min_rtt
}
/// 获取当前时钟漂移率 (ppm)
pub fn get_drift_rate(&self) -> f64 {
self.drift_rate
}
/// 获取同步质量评估 (0-100, 越高越好)
pub fn get_sync_quality(&self) -> u8 {
if self.offsets.is_empty() {
return 0;
}
// 基于 RTT 稳定性和偏移量方差评估
let rtt_variance = self.calculate_variance(&self.rtts.iter().copied().collect::<Vec<_>>());
let offset_variance =
self.calculate_variance(&self.offsets.iter().map(|s| s.offset).collect::<Vec<_>>());
// RTT 越稳定,方差越小,质量越高
let rtt_score = ((100_000.0 - rtt_variance.min(100_000.0)) / 100_000.0 * 50.0) as u8;
let offset_score = ((50_000.0 - offset_variance.min(50_000.0)) / 50_000.0 * 50.0) as u8;
rtt_score + offset_score
}
fn calculate_variance(&self, samples: &[i128]) -> f64 {
if samples.is_empty() {
return 0.0;
}
let mean = samples.iter().sum::<i128>() as f64 / samples.len() as f64;
let variance = samples
.iter()
.map(|&x| {
let diff = x as f64 - mean;
diff * diff
})
.sum::<f64>()
/ samples.len() as f64;
variance.sqrt()
}
}
+1
View File
@@ -0,0 +1 @@
pub mod shell;
+68
View File
@@ -0,0 +1,68 @@
use anyhow::{Context, Result, anyhow};
use core::str;
use serde::{Deserialize, Serialize};
use std::process::Stdio;
use std::time::Duration;
use tokio::io::AsyncReadExt;
use tokio::process::Command;
pub type ShellRequest = String;
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct ShellResponse {
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
}
pub struct Shell;
impl Shell {
pub async fn run(command: String, timeout_ms: Option<u64>) -> Result<ShellResponse> {
// 1. 配置 Command
let mut child = Command::new("sh")
.arg("-c")
.arg(&command)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
// 当 Child 结构体被 Drop 时,自动杀死子进程(SIGKILL)
.kill_on_drop(true)
.spawn()
.context("Failed to spawn shell command")?;
let mut stdout_reader = child.stdout.take().context("Failed to open stdout")?;
let mut stderr_reader = child.stderr.take().context("Failed to open stderr")?;
// 2. 定义执行逻辑
let run_logic = async move {
let mut stdout_buf = Vec::new();
let mut stderr_buf = Vec::new();
// 使用 join! 并发读取流并等待进程结束
let (res_out, res_err, res_status) = tokio::join!(
stdout_reader.read_to_end(&mut stdout_buf),
stderr_reader.read_to_end(&mut stderr_buf),
child.wait()
);
let status = res_status.context("Wait for child failed")?;
res_out.context("Read stdout failed")?;
res_err.context("Read stderr failed")?;
Ok(ShellResponse {
stdout: String::from_utf8_lossy(&stdout_buf).to_string(),
stderr: String::from_utf8_lossy(&stderr_buf).to_string(),
exit_code: status.code().unwrap_or(-1),
})
};
// 3. 应用超时
if let Some(ms) = timeout_ms {
tokio::time::timeout(Duration::from_millis(ms), run_logic)
.await
.map_err(|_| anyhow!("Shell execution timed out"))?
} else {
run_logic.await
}
}
}
+34 -35
View File
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1.4
FROM ubuntu:20.04
FROM --platform=linux/amd64 ubuntu:20.04
# 1. 环境变量合并,减少层数
# 1. 环境变量
ENV DEBIAN_FRONTEND=noninteractive \
PATH="/opt/toolchain/bin:/root/.cargo/bin:${PATH}" \
PKG_CONFIG_ALLOW_CROSS=1 \
@@ -10,8 +10,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc \
CC_armv7_unknown_linux_gnueabihf="arm-linux-gnueabihf-gcc --sysroot=/opt/sysroot"
# 2. 基础依赖与多架构源配置合并
# 使用 --mount=type=cache 优化 apt 操作
# 2. 基础依赖与多架构源
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list && \
@@ -26,8 +25,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
# 删除 lists 文件以减小体积
rm -rf /var/lib/apt/lists/*
# 3. 安装 Linaro Toolchain & Rust
# 将下载和清理放在一层,减少体积
# 3. 工具链
RUN mkdir -p /opt/toolchain && \
curl -fSL https://releases.linaro.org/components/toolchain/binaries/latest-7/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz | tar -xJ -C /opt/toolchain --strip-components=1 && \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable && \
@@ -35,48 +33,49 @@ RUN mkdir -p /opt/toolchain && \
WORKDIR /app
# 4. 脚本与 Rootfs 处理
# 使用 --mount=bind 减少一次 COPY 导致的镜像体积增加
# 4. 脚本与 Rootfs
COPY run.sh runtime.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/run.sh /usr/local/bin/runtime.sh
# 处理 root.squashfs:利用临时挂载或直接解压并删除
# --------------------------------------------------------------------------------
# 5. 构建混合 Sysroot
# 目标:结合 OpenWrt 的运行时库 (root.squashfs) 和 Toolchain/Ubuntu 的开发文件
# --------------------------------------------------------------------------------
RUN --mount=type=bind,source=root.squashfs,target=/app/root.squashfs \
mkdir -p /opt/sysroot && unsquashfs -d /opt/sysroot -f /app/root.squashfs
# 5. 构建混合 Sysroot 逻辑优化
# 将所有文件操作合并到一个 RUN 块,减少 Metadata 冗余
RUN set -e; \
# 1. 自动为共享库创建 .so 软链接
find /opt/sysroot/lib /opt/sysroot/usr/lib -name "lib*.so.*" | while read -r src; do \
set -ex; \
# 1. 解压缩并创建基础目录
mkdir -p /opt/sysroot && unsquashfs -d /opt/sysroot -f /app/root.squashfs; \
mkdir -p /opt/sysroot/usr/lib/pkgconfig /opt/sysroot/usr/include /opt/sysroot/lib; \
\
# 2. 补全启动文件和 nonshared 库
TC_LIBC_DIR="/opt/toolchain/arm-linux-gnueabihf/libc"; \
if [ -d "$TC_LIBC_DIR" ]; then \
find "$TC_LIBC_DIR/usr/lib" -name "*crt*.o" -exec cp -t /opt/sysroot/usr/lib/ {} +; \
find "$TC_LIBC_DIR/usr/lib" \( -name "libc_nonshared.a" -o -name "libpthread_nonshared.a" \) \
-exec cp -t /opt/sysroot/usr/lib/ {} +; \
fi; \
\
# 3. 自动修复所有 .so 软链接
find /opt/sysroot/lib /opt/sysroot/usr/lib -name "lib*.so.*" | while read src; do \
link_name=$(echo "$src" | sed 's/\.so\..*$/.so/'); \
if [ ! -e "$link_name" ]; then \
ln -s "$(basename "$src")" "$link_name"; \
fi; \
fi \
done; \
# 2. 拷贝 crt*.o 文件
find /opt/toolchain/arm-linux-gnueabihf/libc -name "*crt*.o" -exec cp {} /opt/sysroot/usr/lib/ \; ; \
# 3. 拷贝 libc_nonshared.a
cp /opt/toolchain/arm-linux-gnueabihf/libc/usr/lib/libc_nonshared.a /opt/sysroot/usr/lib/; \
if [ -f /opt/toolchain/arm-linux-gnueabihf/libc/usr/lib/libpthread_nonshared.a ]; then \
cp /opt/toolchain/arm-linux-gnueabihf/libc/usr/lib/libpthread_nonshared.a /opt/sysroot/usr/lib/; \
fi; \
# 4. 生成 Linker Scripts
\
# 4. 生成 libc.so Linker Script
rm -f /opt/sysroot/usr/lib/libc.so; \
printf "OUTPUT_FORMAT(elf32-littlearm)\nGROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a AS_NEEDED ( /lib/ld-linux-armhf.so.3 ) )\n" > /opt/sysroot/usr/lib/libc.so; \
if [ -f /opt/sysroot/lib/libpthread.so.0 ]; then \
printf "OUTPUT_FORMAT(elf32-littlearm)\nGROUP ( /lib/libpthread.so.0 /usr/lib/libpthread_nonshared.a )\n" > /opt/sysroot/usr/lib/libpthread.so; \
\
# 5. 注入 Ubuntu 的开发头文件和 PC 文件
if [ -d /usr/lib/arm-linux-gnueabihf/pkgconfig ]; then \
find /usr/lib/arm-linux-gnueabihf/pkgconfig/ -name "*.pc" -exec cp -t /opt/sysroot/usr/lib/pkgconfig/ {} +; \
fi; \
# 5. 注入头文件和 pkgconfig
cp -r /usr/include/alsa /opt/sysroot/usr/include/ 2>/dev/null || true; \
cp -r /usr/include/opus /opt/sysroot/usr/include/ 2>/dev/null || true; \
if [ -d /usr/include/arm-linux-gnueabihf ]; then \
cp -r /usr/include/arm-linux-gnueabihf/* /opt/sysroot/usr/include/ 2>/dev/null || true; \
fi; \
if [ -d /usr/lib/arm-linux-gnueabihf/pkgconfig ]; then \
cp -r /usr/lib/arm-linux-gnueabihf/pkgconfig/* /opt/sysroot/usr/lib/pkgconfig/ 2>/dev/null || true; \
fi
[ -d /usr/include/arm-linux-gnueabihf ] && cp -r /usr/include/arm-linux-gnueabihf/* /opt/sysroot/usr/include/ 2>/dev/null || true
# 6. 设置 Rust 链接参数
# 6. Rust 编译参数
ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_RUSTFLAGS="\
-C link-arg=--sysroot=/opt/sysroot \
-C link-arg=-Wl,-dynamic-linker,/lib/ld-linux-armhf.so.3 \