botserver/src/video/websocket.rs
Rodrigo Rodriguez 5ea171d126
Some checks failed
BotServer CI / build (push) Failing after 1m34s
Refactor: Split large files into modular subdirectories
Split 20+ files over 1000 lines into focused subdirectories for better
maintainability and code organization. All changes maintain backward
compatibility through re-export wrappers.

Major splits:
- attendance/llm_assist.rs (2074→7 modules)
- basic/keywords/face_api.rs → face_api/ (7 modules)
- basic/keywords/file_operations.rs → file_ops/ (8 modules)
- basic/keywords/hear_talk.rs → hearing/ (6 modules)
- channels/wechat.rs → wechat/ (10 modules)
- channels/youtube.rs → youtube/ (5 modules)
- contacts/mod.rs → contacts_api/ (6 modules)
- core/bootstrap/mod.rs → bootstrap/ (5 modules)
- core/shared/admin.rs → admin_*.rs (5 modules)
- designer/canvas.rs → canvas_api/ (6 modules)
- designer/mod.rs → designer_api/ (6 modules)
- docs/handlers.rs → handlers_api/ (11 modules)
- drive/mod.rs → drive_handlers.rs, drive_types.rs
- learn/mod.rs → types.rs
- main.rs → main_module/ (7 modules)
- meet/webinar.rs → webinar_api/ (8 modules)
- paper/mod.rs → (10 modules)
- security/auth.rs → auth_api/ (7 modules)
- security/passkey.rs → (4 modules)
- sources/mod.rs → sources_api/ (5 modules)
- tasks/mod.rs → task_api/ (5 modules)

Stats: 38,040 deletions, 1,315 additions across 318 files

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-12 21:09:30 +00:00

203 lines
6.5 KiB
Rust

use axum::{
extract::{
ws::{Message, WebSocket, WebSocketUpgrade},
Path, State,
},
response::IntoResponse,
};
use futures::{SinkExt, StreamExt};
use std::sync::Arc;
use tokio::sync::broadcast;
use tracing::{error, info, warn};
use uuid::Uuid;
use crate::core::shared::state::AppState;
use super::models::ExportProgressEvent;
static GLOBAL_BROADCASTER: std::sync::OnceLock<Arc<ExportProgressBroadcaster>> =
std::sync::OnceLock::new();
pub struct ExportProgressBroadcaster {
tx: broadcast::Sender<ExportProgressEvent>,
}
impl ExportProgressBroadcaster {
pub fn new() -> Self {
let (tx, _) = broadcast::channel(100);
Self { tx }
}
pub fn global() -> Arc<Self> {
GLOBAL_BROADCASTER
.get_or_init(|| Arc::new(Self::new()))
.clone()
}
pub fn sender(&self) -> broadcast::Sender<ExportProgressEvent> {
self.tx.clone()
}
pub fn subscribe(&self) -> broadcast::Receiver<ExportProgressEvent> {
self.tx.subscribe()
}
pub fn send(&self, event: ExportProgressEvent) {
if let Err(e) = self.tx.send(event) {
warn!("No active WebSocket listeners: {e}");
}
}
}
impl Default for ExportProgressBroadcaster {
fn default() -> Self {
Self::new()
}
}
pub async fn export_progress_websocket(
ws: WebSocketUpgrade,
State(_state): State<Arc<AppState>>,
Path(export_id): Path<Uuid>,
) -> impl IntoResponse {
info!("WebSocket connection request for export: {export_id}");
ws.on_upgrade(move |socket| handle_export_websocket(socket, export_id))
}
async fn handle_export_websocket(socket: WebSocket, export_id: Uuid) {
let (mut sender, mut receiver) = socket.split();
info!("WebSocket connected for export: {export_id}");
let welcome = serde_json::json!({
"type": "connected",
"export_id": export_id.to_string(),
"message": "Connected to export progress stream",
"timestamp": chrono::Utc::now().to_rfc3339()
});
if let Err(e) = sender
.send(Message::Text(welcome.to_string().into()))
.await
{
error!("Failed to send welcome message: {e}");
return;
}
let broadcaster = ExportProgressBroadcaster::global();
let mut progress_rx = broadcaster.subscribe();
let export_id_for_recv = export_id;
let recv_task = tokio::spawn(async move {
while let Some(msg) = receiver.next().await {
match msg {
Ok(Message::Close(_)) => {
info!("WebSocket close requested for export: {export_id_for_recv}");
break;
}
Ok(Message::Ping(_)) => {
info!("Received ping for export: {export_id_for_recv}");
}
Ok(Message::Text(text)) => {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&text) {
if json.get("type").and_then(|v| v.as_str()) == Some("ping") {
info!("Client ping received");
}
}
}
Err(e) => {
error!("WebSocket receive error: {e}");
break;
}
_ => {}
}
}
});
loop {
tokio::select! {
result = progress_rx.recv() => {
match result {
Ok(event) => {
if event.export_id == export_id {
let json = serde_json::json!({
"type": "progress",
"export_id": event.export_id.to_string(),
"project_id": event.project_id.to_string(),
"status": event.status,
"progress": event.progress,
"message": event.message,
"output_url": event.output_url,
"gbdrive_path": event.gbdrive_path,
"timestamp": chrono::Utc::now().to_rfc3339()
});
if let Err(e) = sender.send(Message::Text(json.to_string().into())).await {
error!("Failed to send progress update: {e}");
break;
}
if event.status == "completed" || event.status == "failed" {
let final_msg = serde_json::json!({
"type": "finished",
"export_id": event.export_id.to_string(),
"status": event.status,
"output_url": event.output_url,
"gbdrive_path": event.gbdrive_path
});
let _ = sender.send(Message::Text(final_msg.to_string().into())).await;
break;
}
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
warn!("WebSocket lagged behind by {n} messages");
}
Err(broadcast::error::RecvError::Closed) => {
info!("Progress broadcast channel closed");
break;
}
}
}
_ = tokio::time::sleep(tokio::time::Duration::from_secs(30)) => {
let heartbeat = serde_json::json!({
"type": "heartbeat",
"timestamp": chrono::Utc::now().to_rfc3339()
});
if let Err(e) = sender.send(Message::Text(heartbeat.to_string().into())).await {
error!("Failed to send heartbeat: {e}");
break;
}
}
}
}
recv_task.abort();
info!("WebSocket disconnected for export: {export_id}");
}
pub fn broadcast_export_progress(
export_id: Uuid,
project_id: Uuid,
status: &str,
progress: i32,
message: Option<String>,
output_url: Option<String>,
gbdrive_path: Option<String>,
) {
let event = ExportProgressEvent {
export_id,
project_id,
status: status.to_string(),
progress,
message,
output_url,
gbdrive_path,
};
ExportProgressBroadcaster::global().send(event);
}