feat(live2d): 添加 Live2D 渲染插件及相关依赖,替代 Firefox kiosk 方案

This commit is contained in:
2026-07-07 16:13:40 +08:00
parent 2aac62b2bb
commit 86e36aa677
7 changed files with 510 additions and 44 deletions

View 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 _;