use std::path::Path;
use yog_sync::{Arc, Mutex};
use base64::Engine as _;
use yog_formats::video::Video;
use yog_media::video::Decoder;
use yog_registry::VideoDef;
use crate::design::sources::project::mod_root;
use crate::serve::ServiceError;
const KEPT: usize = 4;
#[derive(yog_data::Schema)]
pub(crate) struct VideoList {
pub(crate) own: Vec<String>,
pub(crate) seconds: Vec<f32>,
pub(crate) images: Vec<String>,
}
#[derive(yog_data::Schema)]
pub(crate) struct Preview {
pub(crate) key: String,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) fps_num: u32,
pub(crate) fps_den: u32,
pub(crate) frames: u32,
pub(crate) sound: Option<String>,
pub(crate) clips: Vec<f32>,
}
#[derive(yog_data::Schema)]
pub(crate) struct Frame {
pub(crate) png: String,
}
static RENDERS: Mutex<Vec<(String, Arc<VideoDef>)>> = Mutex::new(Vec::new());
struct Decoding {
key: String,
decoder: Decoder,
shown: Option<u32>,
rgba: Vec<u8>,
}
static DECODING: Mutex<Option<Decoding>> = Mutex::new(None);
pub(crate) fn video_list(file: &Path) -> Result<VideoList, ServiceError> {
let root = mod_root(file).ok_or("not in a mod")?;
let videos = crate::media::assets::resources_of(&root, yog_formats::yres::Kind::Video);
let seconds = videos
.iter()
.map(|r| crate::media::video::montage::video_seconds(&root, r).unwrap_or(0.0))
.collect();
Ok(VideoList {
own: videos.iter().map(ToString::to_string).collect(),
seconds,
images: crate::media::assets::images(&root),
})
}
pub(crate) fn preview(file: &Path, video: &Video) -> Result<Preview, ServiceError> {
let root = mod_root(file).ok_or("not in a mod")?;
let mut rendered = crate::media::video::render(video, &root)?;
static MADE: yog_sync::atomic::AtomicU64 = yog_sync::atomic::AtomicU64::new(0);
let key = MADE
.fetch_add(1, yog_sync::atomic::Ordering::Relaxed)
.to_string();
let sound = rendered
.sound
.take()
.map(|ogg| base64::engine::general_purpose::STANDARD.encode(ogg));
let def = rendered.def(key.clone(), video, None);
let preview = Preview {
key: key.clone(),
width: def.width,
height: def.height,
fps_num: def.fps_num,
fps_den: def.fps_den,
frames: def.frames(),
sound,
clips: video
.montage
.as_ref()
.map(|m| crate::media::video::montage::durations(m, &root))
.unwrap_or_default(),
};
let mut renders = RENDERS.lock();
renders.retain(|(k, _)| *k != key);
renders.push((key, Arc::new(def)));
let over = renders.len().saturating_sub(KEPT);
renders.drain(..over);
Ok(preview)
}
pub(crate) fn frame(key: &str, frame: u32) -> Result<Frame, ServiceError> {
let def = RENDERS
.lock()
.iter()
.find(|(k, _)| k == key)
.map(|(_, d)| d.clone())
.ok_or_else(|| ServiceError::not_found(format!("no render `{key}` (render it again)")))?;
let mut decoding = DECODING.lock();
if decoding.as_ref().is_none_or(|d| d.key != key) {
*decoding = Some(Decoding {
key: key.to_owned(),
decoder: Decoder::new().map_err(|e| e.to_string())?,
shown: None,
rgba: Vec::new(),
});
}
let d = decoding.as_mut().ok_or("no decoder")?;
decode(d, &def, frame)?;
let image = yog_media::image::Image {
width: def.width,
height: def.height,
rgba: d.rgba.clone(),
};
let png = yog_media::image::encode_png(&image).map_err(|e| e.to_string())?;
Ok(Frame {
png: base64::engine::general_purpose::STANDARD.encode(png),
})
}
fn decode(d: &mut Decoding, def: &VideoDef, target: u32) -> Result<(), ServiceError> {
let plan = def.plan(d.shown, target);
if plan.reset {
d.decoder.reset();
}
let size = (def.width as usize, def.height as usize);
let packets = plan.frames.clone().filter_map(|f| def.packet(f));
if let Err(e) = d.decoder.decode_last(packets, size, &mut d.rgba) {
d.shown = None;
return Err(e.to_string().into());
}
d.shown = plan.frames.clone().next_back().or(d.shown);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use yog_formats::yres::{Entry, Kind, Value};
#[test]
fn a_preview_renders_and_its_frames_decode() {
let root = std::env::temp_dir().join(format!("yog-video-design-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let res = root.join(yog_ids::project::RESOURCES);
std::fs::create_dir_all(res.join(yog_formats::video::DIR)).unwrap();
std::fs::write(
root.join(yog_ids::file::MANIFEST),
"[mod]\nid = \"m\"\nversion = \"0.1.0\"\n",
)
.unwrap();
let entry = Entry {
name: "intro".into(),
kind: Kind::Video,
value: Value::Bytes(crate::media::video::tests::source()),
comment: None,
key: None,
line: 0,
};
std::fs::write(res.join("Videos.yres"), yog_formats::yres::write(&[entry])).unwrap();
let file = res.join(yog_formats::video::DIR).join("intro.yvideo");
assert_eq!(
video_list(&file).unwrap().own,
vec!["@Videos.intro".to_owned()]
);
let video =
yog_data::from_text::<Video>(r#"{ source: "@Videos.intro", width: 64, fps: 10 }"#)
.unwrap()
.value;
let p = preview(&file, &video).unwrap();
assert_eq!((p.width, p.height, p.frames), (64, 36, 20));
assert!(p.sound.is_some());
for f in [0, 5, 19, 3] {
let png = base64::engine::general_purpose::STANDARD
.decode(frame(&p.key, f).unwrap().png)
.unwrap();
let img = yog_media::image::decode(&png).unwrap();
assert_eq!((img.width, img.height), (64, 36));
}
assert!(frame("nope", 0).is_err());
let _ = std::fs::remove_dir_all(&root);
}
}