Compare commits
22
Commits
LX06_1.94.13
..
v2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d187f5d5b | ||
|
|
3cf6771cc9 | ||
|
|
fb6c1fd9f8 | ||
|
|
487a5b8c14 | ||
|
|
0125a1478e | ||
|
|
c0bc4b8ae6 | ||
|
|
d88d63b15b | ||
|
|
cce3dd630a | ||
|
|
052ce06355 | ||
|
|
eeb442d5b8 | ||
|
|
34924fa06e | ||
|
|
c4f7fbec6d | ||
|
|
a2d06958e1 | ||
|
|
24b3fdddf8 | ||
|
|
6f55021651 | ||
|
|
4ed8d3dca9 | ||
|
|
33da56dead | ||
|
|
ef28121f5d | ||
|
|
4d459afd28 | ||
|
|
aa2a5d8474 | ||
|
|
b69bd5a8f9 | ||
|
|
f4340f068f |
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,3 +1,2 @@
|
||||
pub mod alsa;
|
||||
pub mod jitter_buffer;
|
||||
pub mod sync;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
>
|
||||
> 更新版本的固件可能存在变化,导致刷机失败,设备变砖,请自行评估风险。
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
*.mp3
|
||||
*.wav
|
||||
Generated
+853
@@ -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",
|
||||
]
|
||||
@@ -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"] }
|
||||
@@ -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"
|
||||
@@ -0,0 +1,7 @@
|
||||
# Open-XiaoAI Client V2
|
||||
|
||||
> 开发中,敬请期待...
|
||||
|
||||
## License
|
||||
|
||||
MIT License © 2026-PRESENT [Del Wang](https://del.wang)
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod codec;
|
||||
pub mod config;
|
||||
pub mod player;
|
||||
pub mod recorder;
|
||||
pub mod wav;
|
||||
pub mod reader;
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod app;
|
||||
pub mod audio;
|
||||
pub mod net;
|
||||
pub mod utils;
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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((×tamp, _)) = self.buffer.iter().next() {
|
||||
// 检查是否到达播放时间
|
||||
if current_time >= timestamp {
|
||||
let packet = self.buffer.remove(×tamp).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((×tamp, _)) = self.buffer.iter().next() {
|
||||
let packet = self.buffer.remove(×tamp).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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod shell;
|
||||
@@ -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
@@ -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 \
|
||||
|
||||
Reference in New Issue
Block a user