use std::sync::{Arc, Mutex};
use rustmotion::encode::video::FrameTask;
use rustmotion::schema::ResolvedScenario;
pub struct StudioModel {
pub scenario: Arc<ResolvedScenario>,
pub tasks: Arc<Vec<FrameTask>>,
pub total_frames: u32,
pub error: Option<String>,
pub write_error: Option<String>,
pub audio_error: Option<String>,
pub generation: u64,
pub path: Option<std::path::PathBuf>,
pub raw: serde_json::Value,
pub html_source: Option<String>,
}
pub type Shared = Arc<Mutex<StudioModel>>;
impl StudioModel {
pub fn new(
scenario: ResolvedScenario,
error: Option<String>,
path: Option<std::path::PathBuf>,
) -> Self {
let mut html_source = None;
let raw = path
.as_ref()
.and_then(|p| {
let s = std::fs::read_to_string(p).ok()?;
let raw = if rustmotion::loader::is_html_path(p) {
let raw = rustmotion::loader::html_to_scenario_json(&s).ok()?;
let annotations = super::sidecar::read_sidecar(p).unwrap_or_default();
html_source = Some(s.clone());
super::sidecar::merge_annotations(raw, annotations)
} else {
serde_json::from_str(&s).ok()?
};
super::baseline::ensure_baseline(&super::baseline::baseline_slot(), p, &s, &raw);
Some(raw)
})
.unwrap_or(serde_json::Value::Null);
let tasks = rustmotion::encode::build_frame_tasks(&scenario);
let total_frames = tasks.len() as u32;
Self {
scenario: Arc::new(scenario),
tasks: Arc::new(tasks),
total_frames,
error,
write_error: None,
audio_error: None,
generation: 0,
path,
raw,
html_source,
}
}
}
pub fn empty_scenario() -> ResolvedScenario {
use rustmotion::schema::{EasingType, ResolvedView, VideoConfig, ViewType};
ResolvedScenario {
video: VideoConfig {
width: 1920,
height: 1080,
fps: 30,
background: "#000000".to_string(),
codec: None,
crf: None,
},
audio: vec![],
fonts: vec![],
views: vec![ResolvedView {
view_type: ViewType::Slide,
scenes: vec![],
transition: None,
background: Default::default(),
camera_easing: EasingType::Linear,
camera_pan_duration: 0.0,
}],
included_paths: vec![],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opening_a_scenario_does_not_decode_its_audio_on_the_calling_thread() {
let dir = std::env::temp_dir().join(format!("rm-audio-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("with-audio.json");
std::fs::write(
&path,
r##"{ "video": { "width": 64, "height": 36, "background": "#000000" },
"audio": [ { "src": "does-not-exist.wav" } ],
"scenes": [ { "duration": 1.0 } ] }"##,
)
.unwrap();
let scenario = rustmotion::loader::load_input(&path).expect("load");
let model = StudioModel::new(scenario, None, Some(path.clone()));
assert!(
model.audio_error.is_none(),
"a missing track would have reported a failure had the analysis run inline; \
it belongs on the warmup thread, because decoding a 25 MB wav froze the \
library for two seconds before the editor could show its spinner"
);
let _ = std::fs::remove_dir_all(&dir);
}
}