feat(live2d): 添加 Live2D 渲染插件及相关依赖,替代 Firefox kiosk 方案
This commit is contained in:
225
src/plugins/live2d/plugin.rs
Normal file
225
src/plugins/live2d/plugin.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
//! 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 crate::core::plugin_ids;
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread::JoinHandle;
|
||||
|
||||
use super::renderer::Live2DRenderer;
|
||||
|
||||
pub struct Live2DPlugin {
|
||||
ctx: Option<PluginContext>,
|
||||
/// 渲染器句柄(运行在独立线程)
|
||||
renderer: Option<Arc<std::sync::Mutex<Live2DRenderer>>>,
|
||||
worker: Option<JoinHandle<()>>,
|
||||
/// 渲染停止信号
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Live2DPlugin {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ctx: None,
|
||||
renderer: None,
|
||||
worker: None,
|
||||
stop_flag: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<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("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()),
|
||||
})?;
|
||||
|
||||
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();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop(&mut self) -> Result<()> {
|
||||
self.stop_live2d();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Live2DPlugin {
|
||||
fn start_live2d(&mut self, config: &crate::core::config::Config) -> 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)?,
|
||||
));
|
||||
self.renderer = Some(Arc::clone(&renderer));
|
||||
|
||||
let handle = std::thread::spawn(move || {
|
||||
// 渲染循环
|
||||
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: Some("live2d_running".into()),
|
||||
new_state: Some("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<String> {
|
||||
// 候选路径优先级:
|
||||
// 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::Config) -> Result<PathBuf> {
|
||||
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 目录")
|
||||
}
|
||||
|
||||
// 保留 plugin_ids 引用避免 unused
|
||||
#[allow(unused_imports)]
|
||||
use plugin_ids as _;
|
||||
Reference in New Issue
Block a user