rvnano_webrtc_streamer说明文档
rvnano_webrtc_streamer说明文档
本文档详细阐述项目的整体架构、数据流、关键函数作用与逐行代码分析,并对“在老项目基础上新增 WebSocket(LAN)信令”的改动进行完整说明。同时包含前端 lan_frontend/index.html 的关键逻辑设计,解释其如何接收后端发送的视频流。
- 项目根目录:
/home/fuufhjn/rvnano_webrtc_streamer - 老项目:
/home/fuufhjn/rvnano_webrtc_streamer_example - 前端页面:
/home/fuufhjn/lan_frontend/index.html
目录
- 总览与架构
- 从程序入口开始的数据流说明
- 关键逻辑逐行代码分析
- WebRTC 视频采集与封包(SPS/PPS 注入、IDR 前置)
- PeerConnection 配置与 NAT 1:1(LAN 模式优化)
- MQTT 信令实现
- WebSocket 信令实现(LAN 模式)
- 数据通道(DataChannel)
- 与老项目项目的改动对比(新增 WebSocket 信令)
- 前端关键逻辑设计与视频流接收
- 配置文件(options.toml)说明
- 运行建议与调试提示
1. 总览与架构
本项目负责通过 webrtc-rs 以 WebRTC 方式单向发送 H264 视频,并通过 DataChannel 进行控制数据的交互。信令层支持两种模式:
- MQTT 模式:通过 MQTT Broker 进行 SDP/ICE/配置的交换;
- LAN 模式(新增):内置一个 Axum WebSocket 服务,前端直接与设备局域网连接,完成信令交换。
核心模块:
src/main.rs:程序入口,负责加载配置、选择信令模式,驱动事件循环。src/webrtc_app.rs:WebRTC 核心逻辑,PeerConnection 创建、Track 添加、SPS/PPS 注入、ICE/SDP 处理、DataChannel 控制。src/mqtt_signaling.rs:MQTT 信令收发。src/ws_signaling.rs:WebSocket(Axum)信令收发(新增)。src/piped_h264.rs:拉起外部编码器进程,管道读取 H264 NAL,形成 Annex-B 样本。src/robot_ctrl.rs:串口与设备的编码/解码与控制。
2. 从程序入口开始的数据流说明
入口:src/main.rs
#[tokio::main(worker_threads = 1)]
async fn main() -> Result<(), anyhow::Error> {
// 1) 读取配置
let AppOptions { mqtt, webrtc, robot, lan, signaling } = AppOptions::new().unwrap();
// 2) 打开串口
let robot = robot_ctrl::open_serial_port(&robot.serial_port).unwrap();
// 3) 创建 WebRTC 应用:返回
// - rtc_app: 核心对象(持有 PeerConnection/Track 等)
// - rtc_signal_rx: 本地需要发给对端的信令(Ice/Sdp/ConfigAck)
// - rtc_conn_event_rx: 连接生命周期事件(新连接/关闭)
let (mut rtc_app, rtc_signal_rx, rtc_conn_event_rx) =
webrtc_app::RTCApp::new(webrtc, robot).await?;
// 4) 根据 signaling.mode 选择信令实现与远端信令流:
// - MQTT 模式:MQTTApp::new,返回 AsyncClient 与远端信令流
// - LAN 模式:WSApp::new,返回 Axum WebSocket Server 与远端信令流
let (signaling_app, rtc_remote_stream) = match signaling_mode {
"lan" => { let (app, rx) = ws_signaling::WSApp::new(&lan_args).await?; ... }
_ => { let (app, rx) = mqtt_signaling::MQTTApp::new(&mqtt_args).await?; ... }
};
// 5) 事件主循环:四路 select
loop {
tokio::select! {
// (A) 本地 webrtc_app 产生的信令(本端 ICE/SDP/ConfigAck) -> 通过选定信令通道发给对端
rtc_signal_local = rtc_signal_rx.select_next_some() => {
signaling_app.send_signal(rtc_signal_local).await.unwrap();
},
// (B) 远端信令流(MQTT 或 WS) -> 交给 webrtc_app 做处理(ICE/SDP/Config)
rtc_signal_remote = rtc_remote_stream.select_next_some() => {
let _ = rtc_app.handle_signal(rtc_signal_remote).await;
},
// (C) 连接事件(PeerConnection 状态) -> 统计与重启策略
rtc_conn_event = rtc_conn_event_rx.select_next_some() => { ... },
// (D) Ctrl+C 退出
_ = tokio::signal::ctrl_c() => { break; }
}
}
// 6) 关闭并必要时重启进程(释放 webrtc-rs 可能的内存泄漏)
}数据流图(文字):
- 本端(webrtc_app)生成的信令(ICE/SDP/ConfigAck) =>
rtc_signal_rx=>signaling_app.send_signal()=> 发送至对端(MQTT broker 或 WebSocket 客户端) - 对端发来的信令(SDP/ICE/Config) =>
rtc_remote_stream=>rtc_app.handle_signal()=> 设置远端描述/添加候选/创建 PeerConnection - 视频数据:
piped_h264拉起编码器 -> 发送 NAL ->webrtc_app组帧并注入 SPS/PPS ->TrackLocalStaticSample写样本 -> 浏览器端渲染 - 控制:DataChannel 文本消息 JSON 互发;串口读状态回推至 DataChannel。
3. 关键逻辑逐行代码分析
本节精选关键代码片段,逐行解读其功能和设计考量。
3.1 WebRTC 视频采集与封包(SPS/PPS 注入、IDR 前置)
文件:src/webrtc_app.rs 中 RTCApp::new() 启动视频“监督者”,循环拉起编码器并向 rtc_video_track 写样本。
关键片段(简化节选):
let rtc_video_track = Arc::new(TrackLocalStaticSample::new(...));
let video_track2 = Arc::clone(&rtc_video_track);
// Supervisor: keep launching H264 source; auto-restart on EOF/exit
println!("Starting video supervisor: {} bitrate={}", rtc_args.src_executable, rtc_args.video_bitrate);
let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1);
let exec_path = rtc_args.src_executable.clone();
let bitrate = rtc_args.video_bitrate;
let rtp_video_handle = tokio::spawn(async move {
'outer: loop {
println!("Launching video source: {} bitrate={}", exec_path, bitrate);
let (mut h264_nal_stream, audio_socket, mut child) = piped_h264(exec_path.clone(), bitrate);
println!("[webrtc_app] piped_h264 started pid={} bitrate={}", child.id(), bitrate);
// 音频 keepalive 仅用于编码器稳定(并不作为 WebRTC 音频传输)
let (audio_keepalive_tx, mut audio_keepalive_rx) = mpsc::channel::<()>(1);
tokio::spawn(async move {
let buf = vec![0u8; 1600];
loop {
tokio::select! {
_ = audio_keepalive_rx.recv() => { break; }
_ = tokio::time::sleep(Duration::from_millis(20)) => {
let _ = audio_socket.send(&buf).await;
}
}
}
});
// 缓存 SPS/PPS;首次齐备时发送一次“配置样本”
let mut cached_sps: Option<BytesMut> = None;
let mut cached_pps: Option<BytesMut> = None;
let mut spspps_sent: bool = false;
loop {
tokio::select! {
_ = shutdown_rx.recv() => {
// 外部关闭信号 => 结束编码器
let _ = child.kill();
let _ = child.wait();
let _ = audio_keepalive_tx.try_send(());
break 'outer;
}
maybe_nal = h264_nal_stream.recv() => {
match maybe_nal {
Some(nal) => {
// 以低 5 位判定 NALU 类型:7=SPS, 8=PPS, 5=IDR
let nalu_type: u8 = nal.data.as_ref().get(0).copied().unwrap_or(0) & 0x1F;
if nalu_type == 7 { cached_sps = Some(nal.data.clone()); continue; }
if nalu_type == 8 {
cached_pps = Some(nal.data.clone());
// 首次齐备 SPS+PPS 时,发送配置样本,便于浏览器尽早建立解码器
if !spspps_sent {
if let (Some(ref sps), Some(ref pps)) = (&cached_sps, &cached_pps) {
let mut cfg = BytesMut::new();
cfg.extend_from_slice(&[0,0,0,1]); cfg.extend_from_slice(sps.as_ref());
cfg.extend_from_slice(&[0,0,0,1]); cfg.extend_from_slice(pps.as_ref());
let _ = video_track2.write_sample(&Sample { data: cfg.freeze(), duration: Duration::from_millis(0), ..Default::default() }).await;
spspps_sent = true;
}
}
continue;
}
// 构造 Annex-B 格式;若遇到 IDR(5),在前面拼接已缓存的 SPS/PPS
let mut annexb = BytesMut::with_capacity(nal.data.len() + 4);
annexb.extend_from_slice(&[0,0,0,1]);
annexb.extend_from_slice(nal.data.as_ref());
let sample_bytes = if nalu_type == 5 {
let mut au = BytesMut::new();
if let Some(ref sps) = cached_sps {
au.extend_from_slice(&[0,0,0,1]); au.extend_from_slice(sps.as_ref());
}
if let Some(ref pps) = cached_pps {
au.extend_from_slice(&[0,0,0,1]); au.extend_from_slice(pps.as_ref());
}
au.extend_from_slice(annexb.as_ref());
au.freeze()
} else {
annexb.freeze()
};
// 写入到视频 Track(约 33ms)
let _ = video_track2.write_sample(&Sample {
data: sample_bytes,
duration: Duration::from_millis(33),
..Default::default()
}).await;
}
None => {
// 编码器结束 => 停掉 keepalive,等待 child 退出,稍后重试
let _ = audio_keepalive_tx.try_send(());
match child.try_wait() { ... }
tokio::time::sleep(Duration::from_millis(500)).await;
break;
}
}
}
}
}
}
});- 创建
TrackLocalStaticSample:webrtc-rs 的本地静态 Track,用于推送采样数据。 - 外层
'outer监督循环:编码器退出时自动重启,以保证视频持续。 piped_h264()返回(NAL Receiver, UnixDatagram, Child):- 通过 os_pipe 把编码器输出的 H264 NAL 从管道读取;
UnixDatagram用于“音频 keepalive”,保持编码器(或者其媒体链路)稳定。
- NALU 类型判断:不依赖 webrtc-rs 的 H264 NAL enum,直接读首字节低 5 位(兼容性好)。
- SPS/PPS 缓存与首次配置样本发送:
- 浏览器遇到非 IDR 时可能没有解码器配置;提前发送 SPS/PPS 作为独立样本可提高启动兼容性。
- IDR 前置 SPS/PPS:关键帧前注入缓存的 SPS/PPS,确保解码器配置齐备。
- 每个样本写入持续时间
33ms(约 30fps),可根据实际编码器输出调整。
3.2 PeerConnection 配置与 NAT 1:1(LAN 模式优化)
文件:src/webrtc_app.rs setup_peer_connection()。与老项目相比,当前项目对 LAN 模式做了优化,支持关闭 STUN 并通过 NAT 1:1 指定首选主机 IP。
关键片段(简化节选):
// 从 RTCConfig 中读取 STUN/NAT 参数,允许 "none" 关闭 STUN
let stun_trim = cfg.stun.trim().to_string();
let use_stun = !stun_trim.is_empty() && stun_trim.to_lowercase() != "none";
let ice_servers = if use_stun {
match cfg.turn.as_ref() {
None => vec![RTCIceServer { urls: vec![stun_trim], ..Default::default() }],
Some(turn) => vec![ ... ] // 同时配置 STUN 与 TURN
}
} else {
Vec::<RTCIceServer>::new()
};
let config = RTCConfiguration { ice_servers, ..Default::default() };
// H264 编解码参数:声明 constrained-baseline、packetization-mode=1,匹配浏览器偏好
m.register_codec(
RTCRtpCodecParameters {
capability: RTCRtpCodecCapability {
mime_type: MIME_TYPE_H264.to_owned(),
clock_rate: 90000,
channels: 0,
sdp_fmtp_line: "profile-level-id=42e01f;packetization-mode=1;level-asymmetry-allowed=1".to_owned(),
rtcp_feedback: vec![],
},
payload_type: 109,
..Default::default()
},
RTPCodecType::Video,
)?;
// SettingEngine: NAT 1:1(Host 候选),用于指定首选网卡 IP(WiFi/有线)
let mut se = SettingEngine::default();
if let Some(ip) = cfg.preferred_ip.clone() {
if !ip.is_empty() && ip != "0.0.0.0" {
let _ = se.set_nat_1to1_ips(vec![ip], RTCIceCandidateType::Host);
}
}
let api = APIBuilder::new()
.with_media_engine(m)
.with_interceptor_registry(registry)
.with_setting_engine(se)
.build();
let peer_connection = Arc::new(api.new_peer_connection(config).await?);
// 添加视频 Track,设置 ICE/PeerState 回调,DataChannel 回调等
...
self.rtc_signal_tx.send(RTCSignal::Config(cfg)).unwrap();- STUN 可设为
"none"(或空字符串)以禁用,LAN 模式只收集 Host 候选可提高连接稳定性。 SettingEngine::set_nat_1to1_ips()强制候选地址为指定 IP,避免 NAT/多网卡导致的非预期候选地址。- H264 sdp_fmtp 声明使用 constrained-baseline 与
packetization-mode=1,尽量匹配浏览器的 H264(通常 PT 109)。 - 完成 PeerConnection 设定后,发送
RTCSignal::Config(cfg),用于让对端确认并适配。
3.3 MQTT 信令实现
文件:src/mqtt_signaling.rs
核心职责:
- 建立 TLS MQTT 连接,订阅
.../sdp_rx、.../ice_rx、.../rtc_setup; - 将接收到的 JSON 解析为
RTCSignal并推入mqtt_sig_rx流; - 本地 webrtc_app 产生的信令通过
send_signal()发布到对应主题:sdp、ice、rtc_config_ack。
关键片段:
let (client, mut eventloop) = AsyncClient::new(mqtt_options, 10);
client.subscribe(topic_path.clone() + "sdp_rx", QoS::AtLeastOnce).await.unwrap();
client.subscribe(topic_path.clone() + "ice_rx", QoS::AtLeastOnce).await.unwrap();
client.subscribe(topic_path.clone() + "rtc_setup", QoS::AtLeastOnce).await.unwrap();
let (mqtt_sig_tx, mqtt_sig_rx) = mpsc::unbounded_channel::<RTCSignal>();
let mqtt_sig_rx = UnboundedReceiverStream::new(mqtt_sig_rx);
let rx_handle = tokio::spawn(async move {
loop {
let event = eventloop.poll().await;
match &event {
Ok(Event::Incoming(Incoming::Publish(recv_msg))) => {
let payload_str = std::str::from_utf8(&recv_msg.payload).unwrap();
if recv_msg.topic == sdp_topic { tx.send(RTCSignal::Sdp(...)).unwrap(); }
else if recv_msg.topic == ice_topic { tx.send(RTCSignal::Ice(...)).unwrap(); }
else if recv_msg.topic == setup_topic { tx.send(RTCSignal::Config(...)).unwrap(); }
}
Ok(Event::Incoming(Incoming::ConnAck(ack))) => { ... 发布在线状态 ... }
Err(e) => { println!("MQTT Connection error: {e:?}"); sleep(Duration::from_secs(5)).await; }
_ => {}
}
}
});
// 发送端
pub async fn send_signal(&self, sig: RTCSignal) -> Result<(), ClientError> {
let (topic, payload) = match sig { Ice => ("ice"), Sdp => ("sdp"), Config => ("rtc_config_ack") ... };
self.client.publish(topic, QoS::AtLeastOnce, false, payload).await
}- 订阅与发布主题按约定前缀
TOPIC_PREFIX/ClientCN/。 - JSON 协议与
RTCSignal类型严格对应。 - 错误处理:连接错误重试、Lag/异常日志提示。
3.4 WebSocket 信令实现(LAN 模式)
文件:src/ws_signaling.rs(新增)
职责:
- 启动 Axum WebSocket 服务器(路由
/ws); - 将后端信令通过 broadcast 发给所有连接客户端;
- 从客户端接收 JSON
{kind, data},解析为RTCSignal并推送给后端。
关键片段:
pub async fn new(args: &LanOptions) -> Result<(WSApp, impl Stream<Item = RTCSignal>), anyhow::Error> {
let addr: SocketAddr = format!("{}:{}", args.listen_ip, args.listen_port).parse()?;
// client -> backend
let (incoming_tx, incoming_rx) = mpsc::unbounded_channel::<RTCSignal>();
let incoming_rx_stream = UnboundedReceiverStream::new(incoming_rx);
// backend -> clients
let (outgoing_tx, _) = broadcast::channel::<RTCSignal>(256);
let shared_state = SharedState { incoming_tx, outgoing_tx: outgoing_tx.clone() };
let router = Router::new().route("/ws", get(ws_route)).with_state(Arc::new(shared_state));
let listener = tokio::net::TcpListener::bind(addr).await?;
println!("LAN WebSocket signaling listening on {}", addr);
let server_handle = tokio::spawn(async move { axum::serve(listener, router).await?; });
Ok((WSApp { addr, outgoing_tx, _server_handle: server_handle }, incoming_rx_stream))
}
// 写端:后端广播 -> 客户端
fn serialize_signal(sig: &RTCSignal) -> Result<String, serde_json::Error> {
#[serde(tag = "kind", content = "data")]
enum OutMsg<'a> { Sdp(&'a RTCSdp), Ice(&'a RTCIceCandidateInit), ConfigAck(&'a RTCConfig) }
serde_json::to_string(&msg)
}
// 读端:客户端 -> 后端
fn parse_client_message(txt: &str) -> Result<RTCSignal, serde_json::Error> {
#[serde(tag = "kind", content = "data")]
enum InMsg { Sdp(RTCSdp), Ice(RTCIceCandidateInit), Setup(RTCConfig) }
// Setup => RTCSignal::Config
}broadcast::channel保证后端一处发送,多客户端订阅;Lag 情况有日志提示。- 协议统一
kind字段:"sdp"|"ice"|"rtc_setup"与"rtc_config_ack"。 WSApp::send_signal()将后端RTCSignal广播给所有 WebSocket 客户端。
3.5 数据通道(DataChannel)
文件:src/webrtc_app.rs 中 DataChannel 回调
关键片段:
peer_connection.on_data_channel(Box::new(move |d: Arc<RTCDataChannel>| {
...
d.on_open(Box::new(move || {
// 串口接收状态 -> JSON 文本 -> DataChannel
Box::pin(async move {
let mut ctrl_rx = ctrl_rx.lock().await;
loop {
let result = tokio::select! {
msg = ctrl_rx.next() => { d2.send_text(serde_json::to_string(&msg)?) ... }
_ = close_rx.recv() => ControlFlow::Break(())
};
if result.is_break() { break; }
}
let _ = d2.close().await;
})
}));
d.on_message(Box::new(move |msg: DataChannelMessage| {
if msg.is_string {
// 前端传来的命令 JSON -> 解析 -> 串口发送
Box::pin(async move {
let cmd: RobotCommand = serde_json::from_str(&String::from_utf8(msg.data.to_vec())?)?;
ctrl_tx.lock().await.send(cmd).await?;
})
} else { Box::pin(async {}) }
}));
}));on_open启动一个任务:把串口状态通过 DataChannel 发给对端(浏览器或上位机)。on_message解析 JSON 为RobotCommand,通过串口下发。- 关闭时通过
close_tx结束相关任务,健壮性考虑。
4. 与老项目的改动对比(新增 WebSocket 信令)
对比文件与功能:
- 新增文件:
src/ws_signaling.rs:Axum WebSocket 信令服务器与协议序列化/反序列化。
- 修改文件:
src/main.rs:- 新增枚举
SignalingApp::{Mqtt, Ws}; - 从
options.toml读取[signaling]与[lan],根据mode选择信令实现; - 统一事件循环发送/接收信令的逻辑。
- 新增枚举
src/options.rs:- 新增
LanOptions { listen_ip, listen_port }; - 新增
SignalingOptions { mode }("mqtt" 或 "lan"); AppOptions增加lan: Option<LanOptions>, signaling: Option<SignalingOptions>。
- 新增
Cargo.toml:- 增加依赖
axum = { version = "0.7", features = ["ws"] }以支持 WebSocket。
- 增加依赖
src/webrtc_app.rs:- 与老项目相比,当前项目移除了 WebRTC 音频 Track(仅视频),但保留
UnixDatagram用作“音频 keepalive”以稳定编码器; - 增加了对 H264 SPS/PPS 的缓存、首次配置样本、IDR 前置注入(老项目为直接送 NAL);
- 增加
SettingEngine以支持 NAT 1:1 与关闭 STUN 的 LAN 优化;H264 的sdp_fmtp_line设置更贴近浏览器偏好(packetization-mode=1)。
- 与老项目相比,当前项目移除了 WebRTC 音频 Track(仅视频),但保留
src/piped_h264.rs:- 保持接口:返回 NAL Receiver 与一个 UnixDatagram(在当前项目中仅用于 keepalive),并确保通过
FD_CLOEXEC清除把 FD 传给子进程。
- 保持接口:返回 NAL Receiver 与一个 UnixDatagram(在当前项目中仅用于 keepalive),并确保通过
- 老项目文件差异(参考
rvnano_webrtc_streamer_example):- 老项目的
main.rs仅有 MQTT 模式; - 老项目的
webrtc_app.rs同时处理视频与音频(OPUS),并且会在on_track中将接收的 OPUS RTP 转发到UnixDatagram; - 老项目的
options.rs不包含lan与signaling; - 当前项目的
options.toml新增了[signaling]与[lan]配置,默认mode = "lan"。
- 老项目的
5. 前端关键逻辑设计与视频流接收
文件:lan_frontend/index.html
目标:在局域网直接通过 WebSocket 与设备进行信令交换,只接收视频流。
关键流程:
- 用户输入设备 IP 和端口,点击“开始”;
- 前端连接
ws://{ip}:{port}/ws; - 发送
{"kind":"rtc_setup","data":{"stun":"..."}}; - 调用
setupPeerConnection(cfg)创建只接收视频的RTCPeerConnection,设置 H264 优先; - 调用
negotiateAsOfferer():创建 Offer,设置本地描述并发送{"kind":"sdp","data":{"type":"offer","sdp":"..."}}; - 后端返回
answer与ice候选,前端依次处理,必要时将候选先缓存(pendingIce),等待 remoteDescription 设置完成后再添加。
关键代码节选与说明:
ws.onopen = () => {
const cfg = { stun, turn: null };
sendWS("rtc_setup", cfg); // 步骤 3:告知后端配置(允许后端做 NAT 1:1 等适配)
setupPeerConnection(cfg); // 步骤 4:RTCPeerConnection(仅视频)
negotiateAsOfferer().catch(e => log("negotiate error: " + e)); // 步骤 5
};setupPeerConnection(cfg) 重点:
- 创建
RTCPeerConnection({ iceServers }),仅添加video的recvonlyTransceiver; - 通过
RTCRtpReceiver.getCapabilities("video"),将 H264 排在可用编解码器首位; - 使用一个全局
MediaStream(remoteMs)承载所有远端视频 Track,绑定到<video>,避免重复重设srcObject导致pause(); ontrack中将视频 Track 加入remoteMs;onicecandidate事件发送 ICE 候选到后端。
ICE 候选缓存处理:
if (!pc.remoteDescription) {
pendingIce.push(data);
} else {
await pc.addIceCandidate(data);
}自动播放处理:
<video>设置autoplay playsinline muted controls;- 在
loadedmetadata与canplay事件中尝试video.play(),处理浏览器策略可能导致的AbortError。
协议一致性:
- 与后端
ws_signaling.rs保持统一的{kind, data}:"rtc_setup"=> 后端产生RTCSignal::Config=>RTCApp::setup_peer_connection;"sdp",数据包含{ type: "offer" | "answer", sdp: "..." };"ice",数据为标准RTCIceCandidateInit。
6. 配置文件(options.toml)说明
文件:rvnano_webrtc_streamer/options.toml
[mqtt]
ca = "/root/configs/ca.pem"
cert = "/root/configs/robot_01.pem"
key = "/root/configs/robot_01-key.pem"
mqtt_host = "ws.scut.mcurobot.com"
mqtt_port = 10086
[webrtc]
src_executable = "/root/configs/cvi_media_src"
video_bitrate = 1500
[robot]
serial_port = "/dev/ttyS1"
[signaling]
# "lan" 使用内置 WebSocket;"mqtt" 使用 MQTT Broker
mode = "lan"
[lan]
# WebSocket 监听地址与端口
listen_ip = "0.0.0.0"
listen_port = 8080- 选择信令模式:修改
[signaling].mode为"lan"或"mqtt"。 - LAN 模式下,前端需要连接
ws://{设备IP}:{listen_port}/ws。 webrtc.src_executable是外部媒体源程序路径(负责输出 H264 与音频 UDP),项目通过piped_h264启动它。
7. 运行建议与调试提示
- 编译:
cargo build --target riscv64gc-unknown-linux-musl --release - 运行设备端:确保
options.toml配置正确(串口、媒体源路径、LAN 端口等)。 - 浏览器前端:
- 打开
/home/fuufhjn/lan_frontend/index.html; - 输入设备 IP 与端口(与
[lan]监听一致);点击“开始”。
- 打开
- 常见问题:
- 运行一次后再运行会一直尝试重启,这是因为进程资源未被释放导致新进程获取不到,暂时的解决方式就是重启后再运行。
- 依然存在的问题:前端无法显示视频。
附录:函数与模块作用速查
main.rsmain():加载配置、打开串口、初始化RTCApp、选择信令模式、事件循环(信令与连接状态)。SignalingApp::send_signal():统一调用 MQTT 或 WS 的发送接口。
webrtc_app.rsRTCApp::new():创建信令通道、连接事件通道、视频 Track,启动视频监督者(采集 H264、SPS/PPS 注入)。RTCApp::handle_signal():分发Ice/Sdp/Config。handle_sdp():设置远端描述,必要时生成并发送answer。handle_ice():处理远端 ICE 候选(可能入队等待 remote description)。setup_peer_connection():构造 PeerConnection(H264 编解码、默认拦截器、NAT 1:1),注册回调与 DataChannel。
mqtt_signaling.rsMQTTApp::new():TLS MQTT 初始化,订阅主题,产生远端信令流。MQTTApp::send_signal():本地信令发布到对应主题。
ws_signaling.rsWSApp::new():Axum WebSocket 服务启动,配置 client->backend 与 backend->clients 的通道。WSApp::send_signal():后端信令广播到所有连接的 WebSocket 客户端。ws_route/ws_handler():单连接的读写任务,协议序列化/反序列化。serialize_signal()/parse_client_message():统一{kind, data}JSON 协议。
piped_h264.rspiped_h264(exec_path, bitrate):以 FD 方式传递管道与 UDP socket 给子进程,阻塞读取 H264 NAL 并发送到mpsc::Receiver<NAL>。
robot_ctrl.rsRobotCodec:串口协议编解码;状态帧解析,命令帧构建。open_serial_port():打开串口并返回RobotCtrl(包含tx/rx)。
