//! Live2DPlugin — Live2D 原生渲染插件 //! //! 接管 Live2D 模式下的设备端渲染,替代 Firefox kiosk 方案。 //! 监听 ConfigReloaded 消息,当 render_type=Live2d 时启动原生渲染循环。 use crate::core::config::RenderType; use crate::core::message::{Destination, Envelope, Message}; use crate::core::plugin::{Platform, Plugin, PluginContext, PluginInfo}; use anyhow::{Context, Result}; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::thread::JoinHandle; use std::time::{Duration, Instant}; use super::renderer::{Live2DRenderer, Subtitle, SubtitleState}; pub struct Live2DPlugin { ctx: Option, /// 渲染器句柄(运行在独立线程) renderer: Option>>, worker: Option>, /// 渲染停止信号 stop_flag: Arc, /// 字幕共享状态(http 插件写入,渲染线程读取) subtitle: SubtitleState, } impl Live2DPlugin { pub fn new() -> Self { Self { ctx: None, renderer: None, worker: None, stop_flag: Arc::new(AtomicBool::new(false)), subtitle: Arc::new(Mutex::new(None)), } } } impl Default for Live2DPlugin { fn default() -> Self { Self::new() } } impl Plugin for Live2DPlugin { fn id(&self) -> &str { "live2d" } fn info(&self) -> PluginInfo { PluginInfo { name: "Live2D Renderer".to_string(), version: "0.1.0".to_string(), description: "Live2D 原生渲染(Cubism Core FFI + OpenGL ES 2.0)".to_string(), platform: Platform::Linux, } } 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("live2d plugin context is not initialized")?; ctx.tx.send(Envelope { from: self.id().to_string(), to: Destination::Manager, message: Message::PluginReady(self.id().to_string()), })?; // 如果初始配置就是 Live2D 模式,立即启动渲染 let config = (*ctx.config).clone(); if config.character.render_type == RenderType::Live2d { println!("[Live2DPlugin] 检测到初始配置为 Live2D 模式,启动渲染"); self.start_live2d(&config)?; } Ok(()) } fn handle_message(&mut self, msg: Message) -> Result<()> { match msg { Message::ConfigReloaded(config) => { if config.character.render_type == RenderType::Live2d { self.start_live2d(&config)?; } else { self.stop_live2d(); } } Message::Shutdown => { self.stop_live2d(); } Message::Subtitle { text } => { // 设备端字幕:写入共享状态,渲染线程每帧读取,超时自动消失 let ttl = Duration::from_millis( (text.chars().count().max(1) as u64) .saturating_mul(220) .saturating_add(2500), ); *self.subtitle.lock().unwrap() = Some(Subtitle { text, expires_at: Instant::now() + ttl }); } _ => {} } Ok(()) } fn stop(&mut self) -> Result<()> { self.stop_live2d(); Ok(()) } } impl Live2DPlugin { fn start_live2d(&mut self, config: &crate::core::config::AppConfig) -> Result<()> { // 先停止旧的渲染 self.stop_live2d(); let model_rel = config .character .live2d_model .clone() .context("未配置 live2d_model")?; // Core .so 路径(相对项目根目录或绝对路径) let core_so = find_core_so()?; // live2d 资源根目录 let live2d_base = find_live2d_base(config)?; let width = config.display.render_width as i32; let height = config.display.render_height as i32; let stop_flag = Arc::clone(&self.stop_flag); stop_flag.store(false, Ordering::SeqCst); let tx = self .ctx .as_ref() .context("no ctx")? .tx .clone(); let renderer = Arc::new(std::sync::Mutex::new( Live2DRenderer::new( &core_so, &live2d_base, &model_rel, width, height, Arc::clone(&self.subtitle), )?, )); self.renderer = Some(Arc::clone(&renderer)); // 渲染器初始化(含 texture 上传)已完成,释放主线程的 EGL 上下文绑定 // 让渲染子线程能重新绑定(同一个 EGL context 不能同时在两个线程 current) { let r = renderer.lock().unwrap(); r.gl_ctx.release_current(); } let handle = std::thread::spawn(move || { // EGL 上下文是线程绑定的,需在渲染线程重新绑定 { let r = renderer.lock().unwrap(); if let Err(e) = r.gl_ctx.make_current() { eprintln!("[Live2DPlugin] eglMakeCurrent 失败: {e}"); return; } } // 渲染循环 loop { if stop_flag.load(Ordering::SeqCst) { break; } // 检查 talking 状态(从 HTTP 插件共享的状态读取,这里通过轮询 config 实现) // 简化:直接渲染 let render_result = { let mut r = renderer.lock().unwrap(); r.render_frame() }; if let Err(e) = render_result { eprintln!("[Live2DPlugin] 渲染失败: {e}"); let _ = tx.send(Envelope { from: "live2d".to_string(), to: Destination::Manager, message: Message::StateChanged { old_state: "live2d_running".into(), new_state: "live2d_error".into(), }, }); break; } std::thread::sleep(std::time::Duration::from_millis(16)); } }); self.worker = Some(handle); println!("[Live2DPlugin] Live2D 原生渲染已启动: {model_rel}"); Ok(()) } fn stop_live2d(&mut self) { self.stop_flag.store(true, Ordering::SeqCst); if let Some(handle) = self.worker.take() { let _ = handle.join(); } self.renderer = None; } } /// 查找 libLive2DCubismCore.so fn find_core_so() -> Result { // 候选路径优先级: // 1. 环境变量 SHOWEN_LIVE2D_CORE_SO // 2. 项目根 extern/libLive2DCubismCore.so(运行时随二进制一起部署) // 3. /usr/lib/libLive2DCubismCore.so // 4. /home/showen/Showen/ShowenV2/extern/libLive2DCubismCore.so if let Ok(p) = std::env::var("SHOWEN_LIVE2D_CORE_SO") { if std::path::Path::new(&p).exists() { return Ok(p); } } let candidates = [ "extern/libLive2DCubismCore.so", "/usr/lib/libLive2DCubismCore.so", "/usr/local/lib/libLive2DCubismCore.so", "/home/showen/Showen/ShowenV2/extern/libLive2DCubismCore.so", ]; for c in &candidates { if std::path::Path::new(c).exists() { return Ok(c.to_string()); } } anyhow::bail!("找不到 libLive2DCubismCore.so,请设置环境变量 SHOWEN_LIVE2D_CORE_SO 或部署到 extern/") } /// 查找 live2d 资源根目录 fn find_live2d_base(config: &crate::core::config::AppConfig) -> Result { let candidates = [ PathBuf::from("configs/live2d"), PathBuf::from("/home/showen/Showen/ShowenV2/configs/live2d"), std::env::current_dir()?.join("configs/live2d"), ]; for c in &candidates { if c.exists() { return Ok(c.clone()); } } anyhow::bail!("找不到 configs/live2d 目录") }