小爱音箱 OH2P 刷机开启 SSH:让 iPhone 和 Mac 通过 AirPlay 投放
一、最终效果
小米手机原来的投放方式保持不变;iPhone 和 Mac 通过 AirPlay 投放到音箱本机的 xiaoai-airplay。默认优先 AirPlay 2,AirPlay 1 作为兼容和降级路径。
iPhone / Mac → AirPlay 2 优先 → xiaoai-airplay → ALSA default → OH2P 扬声器
└→ vis / ledd → 音乐灯条
- AirPlay 播放经过小爱原生
default音频链路,保留音乐灯条呼吸。 - 喊“小爱同学”时自动压低 AirPlay 音频,语音结束后恢复。
- 新设备开始播放时停止旧音频任务,并真正关闭旧设备的 RTSP 控制连接。
- AirPlay 服务独立运行,不覆盖原有 Agent、语音唤醒和原生启动功能。
二、使用的项目
刷机和获取固件使用 stevenjoezhang/xiaoai-agent,其中固件制作目录是 deploy/client-patch/,macOS 刷机工具是 deploy/flash-tool/,本次设备为 OH2P,实测固件为 1.62.2。
AirPlay 接收端源码使用 hitushen/xiaoai-airplay。刷机时必须同时保留原始和补丁 rootfs:
root.squashfs 原始 rootfs,用于恢复和对照
root-patched.squashfs 补丁 rootfs,用于刷入
三、群晖 AirConnect 为什么没有直接解决
最初群晖上能看到 airupnp 监听 49154,但 iPhone 和 Mac 找不到音箱。小米手机投放可能走小米私有投放或 MiNA 链路,并不代表 OH2P 提供标准 DLNA MediaRenderer。
AirPlay 发送端 → AirConnect → 标准 DLNA/UPnP 播放器
由于 OH2P 对标准 SSDP/DLNA 发现没有稳定响应,最后改为在音箱本机运行 AirPlay 接收端:
iPhone / Mac → xiaoai-airplay → ALSA default → OH2P
四、获取 OTA、制作补丁固件
git clone https://github.com/stevenjoezhang/xiaoai-agent.git
cd xiaoai-agent/deploy/client-patch
cp .env.example .env
.env 只在本地保存,不要提交账号、密码、Token 或 SSH 密码:
MI_USER=<小米数字账号ID>
MI_PASS=<小米账号密码>
MI_DID=<小爱音箱名称或DID>
SSH_PASSWORD=<自己设置的SSH密码>
macOS Apple Silicon 推荐使用 amd64 Docker 环境:
docker build --platform linux/amd64 -t xiaoai-agent-client-patch .
docker run -it --rm --platform linux/amd64 \
--env-file "$(pwd)/.env" \
-v "$(pwd)/assets:/app/assets" \
-v "$(pwd)/patches:/app/patches" \
xiaoai-agent-client-patch
构建流程是:获取匹配 OTA,保存原始 root.squashfs,写入 Dropbear SSH 和 /data/init.sh 补丁,再生成 root-patched.squashfs。
五、备份、刷机和 SSH
刷机前保存原始 OTA、两个 rootfs、设备型号和固件版本,以及 /data 中的重要文件。按项目针对 OH2P 1.62.2 的说明刷入,不要断电或拔 USB。启动后从 Mac 登录:
ssh root@<speaker-ip>
uname -a
ls -la /data/open-xiaoai
ps | grep '[d]ropbear'
netstat -lntup | grep ':22'
若补丁系统无法启动,按项目说明切回原系统启动分区,例如 boot1。
六、部署 Agent
Agent 和 AirPlay 分开部署,Agent 文件放在 /data/open-xiaoai/。Dropbear 不一定提供 SFTP,上传时使用旧 SCP 协议:
scp -O target/armv7-unknown-linux-gnueabihf/release/xiaoai-agent \
root@<speaker-ip>:/data/open-xiaoai/xiaoai-agent
scp -O agent.yaml root@<speaker-ip>:/data/open-xiaoai/agent.yaml
scp -O start-agent.sh root@<speaker-ip>:/data/open-xiaoai/start-agent.sh
先手动确认唤醒、录音、ASR、模型回复和 TTS 正常,再配置开机启动。
七、AirPlay 接收端源码
应用使用本地 vendored shairplay,并启用 AP2:
shairplay = { path = "vendor/shairplay", default-features = false, features = ["ap2"] }
配置使用:
device: default
mode: ap2
default 会经过小爱原生的 vis → ledd 音频链路,不能为了直接出声改成 Playback,否则可能绕过灯效。
协议选择代码:
builder = builder.mode(match config.mode.to_ascii_lowercase().as_str() {
"ap1" => AirPlayMode::AirPlay1,
_ => AirPlayMode::AirPlay2,
});
默认优先 AirPlay 2;只支持传统 RAOP 的设备可以把配置改成 mode: ap1 后重启。AP1 是兼容/降级路径,默认配置仍然是 AP2。
八、多设备接管:真正关闭旧连接
旧版本只停止音频任务时,旧设备会显示“仍连接但没有声音”,因为 RTSP TCP 控制连接还在服务端。现在分两层处理:停止旧音频输出任务,并关闭旧发送端的全部 RTSP TCP 控制连接。
net/server.rs 为每条连接增加 watch 关闭通知:
let (shutdown_tx, shutdown_rx) = watch::channel(false);
process_connection(stream, handler, shutdown_rx).await;
连接循环同时等待 TCP 数据和关闭通知:
tokio::select! {
result = stream.read(&mut buffer) => {
// 正常处理 RTSP 请求
}
changed = shutdown_rx.changed() => {
if changed.is_ok() && *shutdown_rx.borrow() {
break;
}
}
}
raop/connection.rs 按发送端 IP 登记连接。新设备完成音频协商后,只踢掉其他 IP:
pub(crate) fn takeover_sender(&self, keep_ip: IpAddr) {
self.kick_other_clients(keep_ip);
}
同一个发送端 IP 的并行连接会保留,避免 iPhone 在 AP2 协商期间被误踢。AP1 在 RTP SETUP 成功后接管,AP2 在 realtime 或 buffered audio 建立后接管并停止上一个音频任务。
九、灯条、语音压低和开机自启
正常播放:PCM × 1.0
语音活动:PCM × 0.2
语音结束:PCM × 1.0
程序监听 SpeechWakeup、DuplexRecognizeStarted、Dialog.Finish 和 ExitContinuousDialog,只压低 AirPlay PCM,不改变原生 TTS 音量。音频继续走 default → vis → ledd,所以音乐灯条可以保持呼吸。
开机自启不要覆盖原来的 /data/init.sh。先备份,再只增加独立启动命令:
cp -p /data/init.sh /data/init.sh.bak.$(date +%Y%m%d-%H%M%S)
[ -x /data/open-xiaoai/start-airplay.sh ] && \
sh /data/open-xiaoai/start-airplay.sh start > /dev/null 2>&1 &
十、OH2P ARMv7 构建和部署
OH2P 运行时 glibc 较旧,使用 cargo-zigbuild 限制最高 glibc 版本:
RUSTC=~/.rustup/toolchains/1.99.0-aarch64-apple-darwin/bin/rustc \
~/.rustup/toolchains/1.99.0-aarch64-apple-darwin/bin/cargo zigbuild \
--release --target armv7-unknown-linux-gnueabihf.2.25
strings target/armv7-unknown-linux-gnueabihf/release/xiaoai-airplay \
| grep -E '^GLIBC_[0-9]' | sort -u
确认没有高于 GLIBC_2.25 的依赖,再上传并先备份线上二进制:
cd /data/open-xiaoai
cp -p xiaoai-airplay xiaoai-airplay.bak.$(date +%Y%m%d-%H%M%S)
./start-airplay.sh stop
wget -q -O xiaoai-airplay.new http://<mac-ip>:8765/xiaoai-airplay
sha256sum xiaoai-airplay.new
chmod +x xiaoai-airplay.new
mv xiaoai-airplay.new xiaoai-airplay
./start-airplay.sh start
./start-airplay.sh status
十一、验收和回滚
/data/open-xiaoai/start-airplay.sh status
ps w | grep '[x]iaoai-airplay'
netstat -lntup | grep 5000
Mac 发现服务:
dns-sd -B _raop._tcp local
dns-sd -B _airplay._tcp local
真机验收:播放并确认有声、灯条呼吸、语音自动压低和结束后恢复,再用第二台设备接管,确认第一台真正断开。回滚时先停止 AirPlay,恢复带时间戳的 xiaoai-airplay.bak-* 和配置;刷机相关的原始 OTA、rootfs 和 /data 备份不要删除。
十二、总结
xiaoai-agent → 获取 OTA、制作补丁固件、开启 SSH
↓
SSH → 部署自定义 Agent
↓
xiaoai-airplay → AirPlay 2 优先,AirPlay 1 兼容
↓
关闭旧 RTSP 连接 + 停止旧音频任务
↓
ALSA default → 保留 vis / ledd 灯效
↓
语音活动时自动压低 AirPlay
小米手机原来的投放方式仍然可以使用;iPhone 和 Mac 可以通过 AirPlay 投到音箱。
免责声明
本文是个人设备上的技术折腾记录。刷机、修改 rootfs、禁用自动更新都可能导致变砖、失去保修、服务异常或数据丢失。请只在自己拥有或获得授权的设备上操作,先确认型号和版本,并妥善保留原始 OTA、rootfs、设备数据和回滚文件。不要公开小米账号、密码、Token、SSH 密码或私钥。
- 感谢你赐予我前进的力量

