mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-14 03:32:00 +08:00
refactor(events): 将设备状态广播降级为快照同步并按需订阅 WebSocket 事件,顺带修复相关测试
This commit is contained in:
@@ -6,7 +6,6 @@ use std::sync::Arc;
|
||||
|
||||
use crate::config::*;
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::events::SystemEvent;
|
||||
use crate::rtsp::RtspService;
|
||||
use crate::state::AppState;
|
||||
use crate::video::codec_constraints::{
|
||||
@@ -45,73 +44,11 @@ pub async fn apply_video_config(
|
||||
|
||||
let resolution = crate::video::format::Resolution::new(new_config.width, new_config.height);
|
||||
|
||||
// Step 1: 更新 WebRTC streamer 配置(停止现有 pipeline 和 sessions)
|
||||
state
|
||||
.stream_manager
|
||||
.webrtc_streamer()
|
||||
.update_video_config(resolution, format, new_config.fps)
|
||||
.await;
|
||||
tracing::info!("WebRTC streamer config updated");
|
||||
|
||||
// Step 2: 应用视频配置到 streamer(重新创建 capturer)
|
||||
state
|
||||
.stream_manager
|
||||
.streamer()
|
||||
.apply_video_config(&device, format, resolution, new_config.fps)
|
||||
.await
|
||||
.map_err(|e| AppError::VideoError(format!("Failed to apply video config: {}", e)))?;
|
||||
tracing::info!("Video config applied to streamer");
|
||||
|
||||
// Step 3: 重启 streamer(仅 MJPEG 模式)
|
||||
if !state.stream_manager.is_webrtc_enabled().await {
|
||||
if let Err(e) = state.stream_manager.start().await {
|
||||
tracing::error!("Failed to start streamer after config change: {}", e);
|
||||
} else {
|
||||
tracing::info!("Streamer started after config change");
|
||||
}
|
||||
}
|
||||
|
||||
// 配置 WebRTC direct capture(所有模式统一配置)
|
||||
let (device_path, _resolution, _format, _fps, jpeg_quality) = state
|
||||
.stream_manager
|
||||
.streamer()
|
||||
.current_capture_config()
|
||||
.await;
|
||||
if let Some(device_path) = device_path {
|
||||
state
|
||||
.stream_manager
|
||||
.webrtc_streamer()
|
||||
.set_capture_device(device_path, jpeg_quality)
|
||||
.await;
|
||||
} else {
|
||||
tracing::warn!("No capture device configured for WebRTC");
|
||||
}
|
||||
|
||||
if state.stream_manager.is_webrtc_enabled().await {
|
||||
use crate::video::encoder::VideoCodecType;
|
||||
let codec = state
|
||||
.stream_manager
|
||||
.webrtc_streamer()
|
||||
.current_video_codec()
|
||||
.await;
|
||||
let codec_str = match codec {
|
||||
VideoCodecType::H264 => "h264",
|
||||
VideoCodecType::H265 => "h265",
|
||||
VideoCodecType::VP8 => "vp8",
|
||||
VideoCodecType::VP9 => "vp9",
|
||||
}
|
||||
.to_string();
|
||||
let is_hardware = state
|
||||
.stream_manager
|
||||
.webrtc_streamer()
|
||||
.is_hardware_encoding()
|
||||
.await;
|
||||
state.events.publish(SystemEvent::WebRTCReady {
|
||||
transition_id: None,
|
||||
codec: codec_str,
|
||||
hardware: is_hardware,
|
||||
});
|
||||
}
|
||||
|
||||
tracing::info!("Video config applied successfully");
|
||||
Ok(())
|
||||
|
||||
@@ -12,7 +12,6 @@ use tracing::{info, warn};
|
||||
use crate::auth::{Session, SESSION_COOKIE};
|
||||
use crate::config::{AppConfig, StreamMode};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::events::SystemEvent;
|
||||
use crate::state::AppState;
|
||||
use crate::update::{UpdateChannel, UpdateOverviewResponse, UpdateStatusResponse, UpgradeRequest};
|
||||
use crate::video::codec_constraints::codec_to_id;
|
||||
@@ -936,20 +935,8 @@ pub async fn update_config(
|
||||
let resolution =
|
||||
crate::video::format::Resolution::new(new_config.video.width, new_config.video.height);
|
||||
|
||||
// Step 1: Update WebRTC streamer config FIRST
|
||||
// This stops the shared pipeline and closes existing sessions BEFORE capturer is recreated
|
||||
// This ensures the pipeline won't be subscribed to a stale frame source
|
||||
state
|
||||
.stream_manager
|
||||
.webrtc_streamer()
|
||||
.update_video_config(resolution, format, new_config.video.fps)
|
||||
.await;
|
||||
tracing::info!("WebRTC streamer config updated (pipeline stopped, sessions closed)");
|
||||
|
||||
// Step 2: Apply video config to streamer (recreates capturer)
|
||||
if let Err(e) = state
|
||||
.stream_manager
|
||||
.streamer()
|
||||
.apply_video_config(&device, format, resolution, new_config.video.fps)
|
||||
.await
|
||||
{
|
||||
@@ -962,59 +949,6 @@ pub async fn update_config(
|
||||
}));
|
||||
}
|
||||
tracing::info!("Video config applied successfully");
|
||||
|
||||
// Step 3: Start the streamer to begin capturing frames (MJPEG mode only)
|
||||
if !state.stream_manager.is_webrtc_enabled().await {
|
||||
// This is necessary because apply_video_config only creates the capturer but doesn't start it
|
||||
if let Err(e) = state.stream_manager.start().await {
|
||||
tracing::error!("Failed to start streamer after config change: {}", e);
|
||||
// Don't fail the request - the stream might start later when client connects
|
||||
} else {
|
||||
tracing::info!("Streamer started after config change");
|
||||
}
|
||||
}
|
||||
|
||||
// Configure WebRTC direct capture (all modes)
|
||||
let (device_path, _resolution, _format, _fps, jpeg_quality) = state
|
||||
.stream_manager
|
||||
.streamer()
|
||||
.current_capture_config()
|
||||
.await;
|
||||
if let Some(device_path) = device_path {
|
||||
state
|
||||
.stream_manager
|
||||
.webrtc_streamer()
|
||||
.set_capture_device(device_path, jpeg_quality)
|
||||
.await;
|
||||
} else {
|
||||
tracing::warn!("No capture device configured for WebRTC");
|
||||
}
|
||||
|
||||
if state.stream_manager.is_webrtc_enabled().await {
|
||||
use crate::video::encoder::VideoCodecType;
|
||||
let codec = state
|
||||
.stream_manager
|
||||
.webrtc_streamer()
|
||||
.current_video_codec()
|
||||
.await;
|
||||
let codec_str = match codec {
|
||||
VideoCodecType::H264 => "h264",
|
||||
VideoCodecType::H265 => "h265",
|
||||
VideoCodecType::VP8 => "vp8",
|
||||
VideoCodecType::VP9 => "vp9",
|
||||
}
|
||||
.to_string();
|
||||
let is_hardware = state
|
||||
.stream_manager
|
||||
.webrtc_streamer()
|
||||
.is_hardware_encoding()
|
||||
.await;
|
||||
state.events.publish(SystemEvent::WebRTCReady {
|
||||
transition_id: None,
|
||||
codec: codec_str,
|
||||
hardware: is_hardware,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Stream config processing (encoder backend, bitrate, etc.)
|
||||
|
||||
247
src/web/ws.rs
247
src/web/ws.rs
@@ -16,12 +16,122 @@ use axum::{
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::{sync::mpsc, task::JoinHandle};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::events::SystemEvent;
|
||||
use crate::state::AppState;
|
||||
|
||||
enum BusMessage {
|
||||
Event(SystemEvent),
|
||||
Lagged { topic: String, count: u64 },
|
||||
}
|
||||
|
||||
fn normalize_topics(topics: &[String]) -> Vec<String> {
|
||||
let mut normalized = topics.to_vec();
|
||||
normalized.sort();
|
||||
normalized.dedup();
|
||||
|
||||
if normalized.iter().any(|topic| topic == "*") {
|
||||
return vec!["*".to_string()];
|
||||
}
|
||||
|
||||
normalized
|
||||
.into_iter()
|
||||
.filter(|topic| {
|
||||
if topic.ends_with(".*") {
|
||||
return true;
|
||||
}
|
||||
|
||||
let Some((prefix, _)) = topic.split_once('.') else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let wildcard = format!("{}.*", prefix);
|
||||
!topics.iter().any(|candidate| candidate == &wildcard)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_device_info_topic(topic: &str) -> bool {
|
||||
matches!(topic, "*" | "system.*" | "system.device_info")
|
||||
}
|
||||
|
||||
fn rebuild_event_tasks(
|
||||
state: &Arc<AppState>,
|
||||
topics: &[String],
|
||||
event_tx: &mpsc::UnboundedSender<BusMessage>,
|
||||
event_tasks: &mut Vec<JoinHandle<()>>,
|
||||
) {
|
||||
for task in event_tasks.drain(..) {
|
||||
task.abort();
|
||||
}
|
||||
|
||||
let topics = normalize_topics(topics);
|
||||
let mut device_info_task_added = false;
|
||||
for topic in topics {
|
||||
if is_device_info_topic(&topic) && !device_info_task_added {
|
||||
let mut rx = state.subscribe_device_info();
|
||||
let event_tx = event_tx.clone();
|
||||
event_tasks.push(tokio::spawn(async move {
|
||||
if let Some(snapshot) = rx.borrow().clone() {
|
||||
if event_tx.send(BusMessage::Event(snapshot)).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
if rx.changed().await.is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(snapshot) = rx.borrow().clone() {
|
||||
if event_tx.send(BusMessage::Event(snapshot)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
device_info_task_added = true;
|
||||
}
|
||||
|
||||
if is_device_info_topic(&topic) && topic != "*" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(mut rx) = state.events.subscribe_topic(&topic) else {
|
||||
warn!("Client subscribed to unknown topic: {}", topic);
|
||||
continue;
|
||||
};
|
||||
|
||||
let event_tx = event_tx.clone();
|
||||
let topic_name = topic.clone();
|
||||
event_tasks.push(tokio::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(event) => {
|
||||
if event_tx.send(BusMessage::Event(event)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
|
||||
if event_tx
|
||||
.send(BusMessage::Lagged {
|
||||
topic: topic_name.clone(),
|
||||
count,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/// Client-to-server message
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "type", content = "payload")]
|
||||
@@ -50,16 +160,12 @@ pub async fn ws_handler(ws: WebSocketUpgrade, State(state): State<Arc<AppState>>
|
||||
/// Handle WebSocket connection
|
||||
async fn handle_socket(socket: WebSocket, state: Arc<AppState>) {
|
||||
let (mut sender, mut receiver) = socket.split();
|
||||
|
||||
// Subscribe to event bus
|
||||
let mut event_rx = state.events.subscribe();
|
||||
let (event_tx, mut event_rx) = mpsc::unbounded_channel();
|
||||
let mut event_tasks: Vec<JoinHandle<()>> = Vec::new();
|
||||
|
||||
// Track subscribed topics (default: none until client subscribes)
|
||||
let mut subscribed_topics: Vec<String> = vec![];
|
||||
|
||||
// Flag to send device info after first subscribe
|
||||
let mut device_info_sent = false;
|
||||
|
||||
info!("WebSocket client connected");
|
||||
|
||||
// Heartbeat interval (30 seconds)
|
||||
@@ -73,18 +179,13 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>) {
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
if let Err(e) = handle_client_message(&text, &mut subscribed_topics).await {
|
||||
warn!("Failed to handle client message: {}", e);
|
||||
}
|
||||
|
||||
// Send device info after first subscribe
|
||||
if !device_info_sent && !subscribed_topics.is_empty() {
|
||||
let device_info = state.get_device_info().await;
|
||||
if let Ok(json) = serialize_event(&device_info) {
|
||||
if sender.send(Message::Text(json.into())).await.is_err() {
|
||||
warn!("Failed to send device info to client");
|
||||
break;
|
||||
}
|
||||
}
|
||||
device_info_sent = true;
|
||||
} else {
|
||||
rebuild_event_tasks(
|
||||
&state,
|
||||
&subscribed_topics,
|
||||
&event_tx,
|
||||
&mut event_tasks,
|
||||
);
|
||||
}
|
||||
}
|
||||
Some(Ok(Message::Ping(_))) => {
|
||||
@@ -109,28 +210,29 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>) {
|
||||
// Receive event from event bus
|
||||
event = event_rx.recv() => {
|
||||
match event {
|
||||
Ok(event) => {
|
||||
Some(BusMessage::Event(event)) => {
|
||||
// Filter event based on subscribed topics
|
||||
if should_send_event(&event, &subscribed_topics) {
|
||||
if let Ok(json) = serialize_event(&event) {
|
||||
if sender.send(Message::Text(json.into())).await.is_err() {
|
||||
warn!("Failed to send event to client, disconnecting");
|
||||
break;
|
||||
}
|
||||
if let Ok(json) = serialize_event(&event) {
|
||||
if sender.send(Message::Text(json.into())).await.is_err() {
|
||||
warn!("Failed to send event to client, disconnecting");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||
warn!("WebSocket client lagged by {} events", n);
|
||||
Some(BusMessage::Lagged { topic, count }) => {
|
||||
warn!(
|
||||
"WebSocket client lagged by {} events on topic {}",
|
||||
count, topic
|
||||
);
|
||||
// Send error notification to client using SystemEvent::Error
|
||||
let error_event = SystemEvent::Error {
|
||||
message: format!("Lagged by {} events", n),
|
||||
message: format!("Lagged by {} events", count),
|
||||
};
|
||||
if let Ok(json) = serialize_event(&error_event) {
|
||||
let _ = sender.send(Message::Text(json.into())).await;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
None => {
|
||||
warn!("Event bus closed");
|
||||
break;
|
||||
}
|
||||
@@ -147,6 +249,10 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>) {
|
||||
}
|
||||
}
|
||||
|
||||
for task in event_tasks {
|
||||
task.abort();
|
||||
}
|
||||
|
||||
info!("WebSocket handler exiting");
|
||||
}
|
||||
|
||||
@@ -176,21 +282,6 @@ async fn handle_client_message(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if an event should be sent based on subscribed topics
|
||||
fn should_send_event(event: &SystemEvent, topics: &[String]) -> bool {
|
||||
if topics.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fast path: check for wildcard subscription (avoid String allocation)
|
||||
if topics.iter().any(|t| t == "*") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if event matches any subscribed topic
|
||||
topics.iter().any(|topic| event.matches_topic(topic))
|
||||
}
|
||||
|
||||
/// Serialize event to JSON string
|
||||
fn serialize_event(event: &SystemEvent) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string(event)
|
||||
@@ -199,53 +290,49 @@ fn serialize_event(event: &SystemEvent) -> Result<String, serde_json::Error> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::events::SystemEvent;
|
||||
|
||||
#[test]
|
||||
fn test_should_send_event_wildcard() {
|
||||
let event = SystemEvent::StreamStateChanged {
|
||||
state: "streaming".to_string(),
|
||||
device: None,
|
||||
};
|
||||
fn test_normalize_topics_dedupes_and_sorts() {
|
||||
let topics = vec![
|
||||
"stream.state_changed".to_string(),
|
||||
"stream.state_changed".to_string(),
|
||||
"system.device_info".to_string(),
|
||||
];
|
||||
|
||||
assert!(should_send_event(&event, &["*".to_string()]));
|
||||
assert_eq!(
|
||||
normalize_topics(&topics),
|
||||
vec![
|
||||
"stream.state_changed".to_string(),
|
||||
"system.device_info".to_string()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_send_event_prefix() {
|
||||
let event = SystemEvent::StreamStateChanged {
|
||||
state: "streaming".to_string(),
|
||||
device: None,
|
||||
};
|
||||
|
||||
assert!(should_send_event(&event, &["stream.*".to_string()]));
|
||||
assert!(!should_send_event(&event, &["msd.*".to_string()]));
|
||||
fn test_normalize_topics_wildcard_wins() {
|
||||
let topics = vec!["*".to_string(), "stream.state_changed".to_string()];
|
||||
assert_eq!(normalize_topics(&topics), vec!["*".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_send_event_exact() {
|
||||
let event = SystemEvent::StreamStateChanged {
|
||||
state: "streaming".to_string(),
|
||||
device: None,
|
||||
};
|
||||
fn test_normalize_topics_drops_exact_when_prefix_exists() {
|
||||
let topics = vec![
|
||||
"stream.*".to_string(),
|
||||
"stream.state_changed".to_string(),
|
||||
"system.device_info".to_string(),
|
||||
];
|
||||
|
||||
assert!(should_send_event(
|
||||
&event,
|
||||
&["stream.state_changed".to_string()]
|
||||
));
|
||||
assert!(!should_send_event(
|
||||
&event,
|
||||
&["stream.config_changed".to_string()]
|
||||
));
|
||||
assert_eq!(
|
||||
normalize_topics(&topics),
|
||||
vec!["stream.*".to_string(), "system.device_info".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_send_event_empty_topics() {
|
||||
let event = SystemEvent::StreamStateChanged {
|
||||
state: "streaming".to_string(),
|
||||
device: None,
|
||||
};
|
||||
|
||||
assert!(!should_send_event(&event, &[]));
|
||||
fn test_is_device_info_topic_matches_expected_topics() {
|
||||
assert!(is_device_info_topic("system.device_info"));
|
||||
assert!(is_device_info_topic("system.*"));
|
||||
assert!(is_device_info_topic("*"));
|
||||
assert!(!is_device_info_topic("stream.*"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user