Files
ShowenV2/src/plugins/video/mod.rs

369 lines
11 KiB
Rust

//! 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<PluginContext>,
processor: Option<Arc<Mutex<VideoProcessor>>>,
worker: Option<JoinHandle<()>>,
}
impl VideoPlugin {
pub fn new() -> Self {
Self {
ctx: None,
processor: None,
worker: None,
}
}
fn processor(&self) -> Result<&Arc<Mutex<VideoProcessor>>> {
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<String>, new_state: Option<String>) {
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<String> {
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<Mutex<VideoProcessor>>,
tx: std::sync::mpsc::Sender<Envelope>,
) -> 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<Envelope>,
processor: &Arc<Mutex<VideoProcessor>>,
) -> Result<()> {
let status = lock_processor(processor)?.status();
publish_status_message(tx, status)
}
fn publish_status_message(
tx: &std::sync::mpsc::Sender<Envelope>,
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<Envelope>,
old_state: Option<String>,
new_state: Option<String>,
) -> 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<Mutex<VideoProcessor>>,
) -> Result<MutexGuard<'_, VideoProcessor>> {
processor
.lock()
.map_err(|_| anyhow!("video processor lock poisoned"))
}