//! VideoPlugin — 视频播放引擎 //! //! 基于 OpenCV 的视频播放,支持状态机驱动、帧变换、过渡效果。 pub mod processor; pub mod state_machine; use crate::core::message::{Destination, Envelope, Message, PlayerCommand, PlayerStatusData}; use crate::core::plugin::{Platform, Plugin, PluginContext, PluginInfo}; use anyhow::{anyhow, Context, Result}; use opencv::highgui; use processor::VideoProcessor; use std::sync::{Arc, Mutex, MutexGuard}; use std::thread::JoinHandle; pub struct VideoPlugin { ctx: Option, processor: Option>>, worker: Option>, } impl VideoPlugin { pub fn new() -> Self { Self { ctx: None, processor: None, worker: None, } } fn processor(&self) -> Result<&Arc>> { self.processor .as_ref() .context("video processor is not initialized") } fn publish_status(&self) { let Some(ctx) = &self.ctx else { return; }; let Some(processor) = &self.processor else { return; }; let status = match processor.lock() { Ok(processor) => processor.status(), Err(_) => return, }; if let Err(error) = ctx.tx.send(Envelope { from: self.id().to_string(), to: Destination::Broadcast, message: Message::PlayerStatus(status), }) { eprintln!("[VideoPlugin] failed to publish status: {error}"); } } fn publish_state_changed(&self, old_state: Option, new_state: Option) { let Some(ctx) = &self.ctx else { return; }; let (Some(old_state), Some(new_state)) = (old_state, new_state) else { return; }; if old_state == new_state { return; } if let Err(error) = ctx.tx.send(Envelope { from: self.id().to_string(), to: Destination::Broadcast, message: Message::StateChanged { old_state, new_state, }, }) { eprintln!("[VideoPlugin] failed to publish state change: {error}"); } } } impl Default for VideoPlugin { fn default() -> Self { Self::new() } } impl Plugin for VideoPlugin { fn id(&self) -> &str { "video" } fn info(&self) -> PluginInfo { PluginInfo { name: "Video Player".to_string(), version: "0.2.0".to_string(), description: "视频播放引擎 (OpenCV)".to_string(), platform: Platform::Any, } } fn dependencies(&self) -> Vec { vec![] } fn init(&mut self, ctx: PluginContext) -> Result<()> { self.ctx = Some(ctx); Ok(()) } fn start(&mut self) -> Result<()> { let ctx = self .ctx .as_ref() .context("video plugin context is not initialized")?; let processor = Arc::new(Mutex::new(VideoProcessor::new((*ctx.config).clone())?)); let worker_processor = Arc::clone(&processor); let tx = ctx.tx.clone(); let handle = std::thread::spawn(move || { if let Err(error) = run_processor_loop(worker_processor, tx) { eprintln!("[VideoPlugin] playback loop failed: {error}"); } }); self.processor = Some(processor); self.worker = Some(handle); ctx.tx.send(Envelope { from: self.id().to_string(), to: Destination::Manager, message: Message::PluginReady(self.id().to_string()), })?; self.publish_status(); Ok(()) } fn handle_message(&mut self, msg: Message) -> Result<()> { match msg { Message::PlayerCommand(command) => { let processor = Arc::clone(self.processor()?); let mut processor = lock_processor(&processor)?; match command { PlayerCommand::Play => { processor.play()?; // 恢复播放时重新获取防息屏锁 if let Some(ctx) = &self.ctx { let _ = ctx.tx.send(Envelope { from: self.id().to_string(), to: Destination::Plugin("screen".to_string()), message: Message::ScreenLockRequest(true), }); } } PlayerCommand::Pause => { processor.pause(); // 暂停时释放防息屏锁 if let Some(ctx) = &self.ctx { let _ = ctx.tx.send(Envelope { from: self.id().to_string(), to: Destination::Plugin("screen".to_string()), message: Message::ScreenLockRequest(false), }); } } PlayerCommand::Next => { processor.next_video()?; } PlayerCommand::Previous => { processor.previous()?; } PlayerCommand::Goto(index) => { processor.goto(index)?; } PlayerCommand::ChangeScene(name) => { processor.change_scene(&name)?; } } drop(processor); self.publish_status(); } Message::Trigger { name, value } => { let processor = Arc::clone(self.processor()?); let mut processor = lock_processor(&processor)?; let old_state = processor.current_state().map(str::to_owned); processor.trigger(&name, &value)?; let new_state = processor.current_state().map(str::to_owned); drop(processor); self.publish_state_changed(old_state, new_state); self.publish_status(); } Message::ConfigReloaded(config) => { let processor = Arc::new(Mutex::new(VideoProcessor::new(config)?)); if let Some(old) = self.processor.replace(Arc::clone(&processor)) { if let Ok(mut old) = old.lock() { let _ = old.stop(); } } if let Some(handle) = self.worker.take() { let _ = handle.join(); } let worker_processor = Arc::clone(&processor); let tx = self .ctx .as_ref() .context("video plugin context is not initialized")? .tx .clone(); self.worker = Some(std::thread::spawn(move || { if let Err(error) = run_processor_loop(worker_processor, tx) { eprintln!("[VideoPlugin] playback loop failed after reload: {error}"); } })); self.publish_status(); } Message::Shutdown => { self.stop()?; } _ => {} } Ok(()) } fn stop(&mut self) -> Result<()> { if let Some(processor) = &self.processor { if let Ok(mut processor) = processor.lock() { let _ = processor.stop(); } } if let Some(handle) = self.worker.take() { let _ = handle.join(); } self.publish_status(); Ok(()) } } fn run_processor_loop( processor: Arc>, tx: std::sync::mpsc::Sender, ) -> Result<()> { { let mut processor = lock_processor(&processor)?; processor.start()?; } publish_processor_status(&tx, &processor)?; loop { let (outcome, old_state, new_state, old_status, new_status) = { let mut processor = lock_processor(&processor)?; let old_state = processor.current_state().map(str::to_owned); let old_status = processor.status(); let outcome = processor.step()?; let new_state = processor.current_state().map(str::to_owned); let new_status = processor.status(); (outcome, old_state, new_state, old_status, new_status) }; if let Some(frame) = outcome.frame { let processor = lock_processor(&processor)?; processor.display_frame(&outcome.window_name, &frame)?; } if old_state != new_state { publish_state_changed(&tx, old_state, new_state)?; } if status_changed(&old_status, &new_status) { publish_status_message(&tx, new_status.clone())?; } if !outcome.keep_running { break; } let key = highgui::wait_key(outcome.delay)?; let (old_state, new_state, old_status, new_status) = { let mut processor = lock_processor(&processor)?; let old_state = processor.current_state().map(str::to_owned); let old_status = processor.status(); processor.handle_key_code(key)?; let new_state = processor.current_state().map(str::to_owned); let new_status = processor.status(); (old_state, new_state, old_status, new_status) }; if old_state != new_state { publish_state_changed(&tx, old_state, new_state)?; } if status_changed(&old_status, &new_status) { publish_status_message(&tx, new_status.clone())?; } if !new_status.running { break; } } let mut processor = lock_processor(&processor)?; processor.stop() } fn publish_processor_status( tx: &std::sync::mpsc::Sender, processor: &Arc>, ) -> Result<()> { let status = lock_processor(processor)?.status(); publish_status_message(tx, status) } fn publish_status_message( tx: &std::sync::mpsc::Sender, status: PlayerStatusData, ) -> Result<()> { tx.send(Envelope { from: "video".to_string(), to: Destination::Broadcast, message: Message::PlayerStatus(status), })?; Ok(()) } fn publish_state_changed( tx: &std::sync::mpsc::Sender, old_state: Option, new_state: Option, ) -> Result<()> { let (Some(old_state), Some(new_state)) = (old_state, new_state) else { return Ok(()); }; if old_state == new_state { return Ok(()); } tx.send(Envelope { from: "video".to_string(), to: Destination::Broadcast, message: Message::StateChanged { old_state, new_state, }, })?; Ok(()) } fn status_changed(old: &PlayerStatusData, new: &PlayerStatusData) -> bool { old != new } fn lock_processor( processor: &Arc>, ) -> Result> { processor .lock() .map_err(|_| anyhow!("video processor lock poisoned")) }