369 lines
11 KiB
Rust
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"))
|
|
}
|