This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

✨ Improve engine controllers API

+177 -156
+101 -1
src/video/encoders/mod.rs
··· 1 - use crate::video::engine::EngineOutput; 1 + use crate::{ 2 + Video, 3 + ui::{Log, Pretty}, 4 + video::{encoders::vgv::VGVTranscodeMode, engine::EngineOutput}, 5 + }; 2 6 use anyhow::Result; 7 + use std::path::PathBuf; 3 8 4 9 pub mod ffmpeg; 5 10 pub mod vgv; ··· 13 18 format!("{}/{} frames", current, total,) 14 19 } 15 20 } 21 + 22 + impl<C: Default> Video<C> { 23 + pub(crate) fn setup_encoder( 24 + &mut self, 25 + output_path: impl Into<PathBuf>, 26 + ) -> Result<Box<dyn Encoder + Send>> { 27 + let (width, height) = 28 + self.initial_canvas.resolution_to_size_even(self.resolution); 29 + 30 + let destination = output_path.into(); 31 + let pb = &self.progress_bars.encoding; 32 + 33 + if destination.exists() { 34 + std::fs::remove_file(&destination)?; 35 + } 36 + 37 + std::fs::create_dir_all( 38 + &destination 39 + .parent() 40 + .expect("Given output file has no parent"), 41 + )?; 42 + 43 + Ok(match destination.full_extension() { 44 + ".vgv.html" => { 45 + self.progress_bars.encoding.log( 46 + "Selecting", 47 + &format!( 48 + "VGV encoder with HTML transcoding as {} ends with .vgv.html", 49 + destination.pretty(), 50 + ), 51 + ); 52 + 53 + Box::new(self.setup_vgv_encoder( 54 + VGVTranscodeMode::ToHTML, 55 + width as _, 56 + height as _, 57 + &self.initial_canvas, 58 + destination, 59 + )?) 60 + } 61 + ".vgv" => { 62 + self.progress_bars.encoding.log( 63 + "Selecting", 64 + &format!( 65 + "VGV encoder as {} ends with .vgv (use .vgv.html for HTML transcoding)", 66 + destination.pretty(), 67 + ), 68 + ); 69 + 70 + Box::new(self.setup_vgv_encoder( 71 + VGVTranscodeMode::None, 72 + width as _, 73 + height as _, 74 + &self.initial_canvas, 75 + destination, 76 + )?) 77 + } 78 + _ => { 79 + pb.log( 80 + "Selecting", 81 + &format!( 82 + "FFMpeg encoder as {} ends with {}", 83 + destination.pretty(), 84 + destination.full_extension() 85 + ), 86 + ); 87 + 88 + self.initial_canvas.load_fonts()?; 89 + Box::new(self.setup_ffmpeg_encoder(width, height, destination)?) 90 + } 91 + }) 92 + } 93 + } 94 + 95 + 96 + // Because .extension() sucks 97 + 98 + trait FullExtension { 99 + fn full_extension(&self) -> &str; 100 + } 101 + 102 + impl FullExtension for PathBuf { 103 + fn full_extension(&self) -> &str { 104 + let filename = self 105 + .file_name() 106 + .and_then(|f| f.to_str()) 107 + .unwrap_or_default(); 108 + let parts: Vec<&str> = filename.split('.').collect(); 109 + if parts.len() <= 1 { 110 + "" 111 + } else { 112 + &filename[filename.find('.').unwrap()..] 113 + } 114 + } 115 + }
+31 -97
src/video/encoding.rs
··· 1 1 use super::Video; 2 + use crate::Timestamp; 3 + use crate::context::Context; 2 4 use crate::ui::{Log, Pretty}; 3 5 use crate::video::encoders::Encoder; 4 - use crate::video::encoders::vgv::VGVTranscodeMode; 5 - use crate::video::engine::EngineOutput; 6 + use crate::video::engine::{EngineControl, EngineController, EngineOutput}; 6 7 use anyhow::{Result, anyhow}; 7 8 use measure_time::debug_time; 8 9 use std::path::PathBuf; 9 10 use std::thread; 10 11 11 - impl<AdditionalContext: Default> Video<AdditionalContext> { 12 + impl<C: Default> Video<C> { 12 13 pub fn encode( 13 14 &mut self, 14 15 output_file: impl Into<PathBuf> + Clone, 15 16 ) -> Result<std::time::Duration> { 17 + let actual_ms_range = self.constrained_ms_range(); 18 + if actual_ms_range != self.total_ms_range() { 19 + self.progress_bars.rendering.log( 20 + "Constrained", 21 + &Timestamp::from_ms_range(&actual_ms_range).pretty(), 22 + ); 23 + } 24 + 25 + self.encode_controlled(output_file, &move |ctx| { 26 + if actual_ms_range.contains(&ctx.ms) { 27 + EngineControl::Render 28 + } else if ctx.ms > actual_ms_range.end { 29 + EngineControl::Stop 30 + } else { 31 + EngineControl::Skip 32 + } 33 + }) 34 + } 35 + 36 + pub fn encode_controlled( 37 + &mut self, 38 + output_file: impl Into<PathBuf> + Clone, 39 + engine_controller: &EngineController<C>, 40 + ) -> Result<std::time::Duration> { 16 41 debug_time!("encode"); 17 42 18 43 let encoder = self.setup_encoder(output_file.clone())?; 19 44 let encoder_name = encoder.name(); 20 45 21 - let time_taken = self.encode_with(encoder)?; 46 + let time_taken = self.encode_with(encoder, engine_controller)?; 22 47 23 48 let _ = notify_rust::Notification::new() 24 49 .appname("Shapemaker") ··· 32 57 Ok(time_taken) 33 58 } 34 59 35 - fn setup_encoder( 36 - &mut self, 37 - output_path: impl Into<PathBuf>, 38 - ) -> Result<Box<dyn Encoder + Send>> { 39 - let (width, height) = 40 - self.initial_canvas.resolution_to_size_even(self.resolution); 41 - 42 - let destination = output_path.into(); 43 - let pb = &self.progress_bars.encoding; 44 - 45 - if destination.exists() { 46 - std::fs::remove_file(&destination)?; 47 - } 48 - 49 - std::fs::create_dir_all( 50 - &destination 51 - .parent() 52 - .expect("Given output file has no parent"), 53 - )?; 54 - 55 - Ok(match destination.full_extension() { 56 - ".vgv.html" => { 57 - self.progress_bars.encoding.log( 58 - "Selecting", 59 - &format!( 60 - "VGV encoder with HTML transcoding as {} ends with .vgv.html", 61 - destination.pretty(), 62 - ), 63 - ); 64 - 65 - Box::new(self.setup_vgv_encoder( 66 - VGVTranscodeMode::ToHTML, 67 - width as _, 68 - height as _, 69 - &self.initial_canvas, 70 - destination, 71 - )?) 72 - } 73 - ".vgv" => { 74 - self.progress_bars.encoding.log( 75 - "Selecting", 76 - &format!( 77 - "VGV encoder as {} ends with .vgv (use .vgv.html for HTML transcoding)", 78 - destination.pretty(), 79 - ), 80 - ); 81 - 82 - Box::new(self.setup_vgv_encoder( 83 - VGVTranscodeMode::None, 84 - width as _, 85 - height as _, 86 - &self.initial_canvas, 87 - destination, 88 - )?) 89 - } 90 - _ => { 91 - pb.log( 92 - "Selecting", 93 - &format!( 94 - "FFMpeg encoder as {} ends with {}", 95 - destination.pretty(), 96 - destination.full_extension() 97 - ), 98 - ); 99 - 100 - self.initial_canvas.load_fonts()?; 101 - Box::new(self.setup_ffmpeg_encoder(width, height, destination)?) 102 - } 103 - }) 104 - } 105 - 106 60 pub fn encode_with( 107 61 &mut self, 108 62 mut encoder: Box<dyn Encoder + Send>, 63 + engine_controller: &EngineController<C>, 109 64 ) -> Result<std::time::Duration> { 110 65 debug_time!("encode_with"); 111 66 ··· 146 101 Ok(time_taken) 147 102 }); 148 103 149 - self.render_with_overrides(tx)?; 104 + self.render(tx, engine_controller)?; 150 105 151 106 let time_taken = encoder_thread 152 107 .join() ··· 165 120 ); 166 121 } 167 122 } 168 - 169 - // Because .extension() sucks 170 - 171 - trait FullExtension { 172 - fn full_extension(&self) -> &str; 173 - } 174 - 175 - impl FullExtension for PathBuf { 176 - fn full_extension(&self) -> &str { 177 - let filename = self 178 - .file_name() 179 - .and_then(|f| f.to_str()) 180 - .unwrap_or_default(); 181 - let parts: Vec<&str> = filename.split('.').collect(); 182 - if parts.len() <= 1 { 183 - "" 184 - } else { 185 - &filename[filename.find('.').unwrap()..] 186 - } 187 - } 188 - }
+33 -51
src/video/engine.rs
··· 1 1 use super::{Video, context::Context}; 2 + use crate::SVGRenderable; 2 3 use crate::rendering::svg; 3 4 use crate::ui::{Log, Pretty}; 4 - use crate::{SVGRenderable, Timestamp}; 5 5 use anyhow::Result; 6 6 use measure_time::debug_time; 7 7 use std::sync::mpsc::SyncSender; 8 + 9 + pub type EngineController<C: Default> = dyn Fn(&Context<'_, C>) -> EngineControl; 8 10 9 11 /// What data is sent to the output by the rendering engine for each rendered frame 10 12 pub enum EngineOutput { ··· 31 33 } 32 34 } 33 35 34 - impl<AdditionalContext: Default> Video<AdditionalContext> { 36 + impl<C: Default> Video<C> { 35 37 pub fn render( 36 38 &self, 37 39 output: SyncSender<EngineOutput>, 38 - controller: impl Fn(&Context<AdditionalContext>) -> EngineControl, 40 + controller: &EngineController<C>, 39 41 ) -> Result<usize> { 40 42 debug_time!("render"); 41 43 ··· 45 47 current_scene: None, 46 48 fps: self.fps, 47 49 syncdata: &self.syncdata, 48 - extra: AdditionalContext::default(), 50 + extra: C::default(), 49 51 inner_hooks: vec![], 50 52 audiofile: self.audiofile.clone(), 51 53 duration_override: self.duration_override, ··· 89 91 90 92 pb.inc(1); 91 93 pb.set_message(match context.current_scene { 92 - Some(ref scene) => format!("{}: {scene}", context.timestamp()), 94 + Some(ref scene) => { 95 + format!("{}: {scene}", context.timestamp()) 96 + } 93 97 None => format!("{}", context.timestamp()), 94 98 }); 95 99 ··· 141 145 } 142 146 } 143 147 144 - if !skip_rendering && context.frame() != previous_rendered_frame { 145 - output.send(EngineOutput::Frame { 146 - dimensions: (canvas.width(), canvas.height()), 147 - svg: canvas.render_to_svg( 148 - canvas.colormap.clone(), 149 - canvas.cell_size, 150 - canvas.object_sizes, 151 - "", 152 - )?, 153 - })?; 148 + if context.frame() != previous_rendered_frame { 149 + if !skip_rendering { 150 + output.send(EngineOutput::Frame { 151 + dimensions: (canvas.width(), canvas.height()), 152 + svg: canvas.render_to_svg( 153 + canvas.colormap.clone(), 154 + canvas.cell_size, 155 + canvas.object_sizes, 156 + "", 157 + )?, 158 + })?; 159 + } 154 160 155 161 context.rendered_frames += 1; 156 162 ··· 179 185 Ok(context.rendered_frames) 180 186 } 181 187 182 - pub fn render_single_frame(&self, frame_no: usize) -> Result<svg::Node> { 188 + /// Render a single frame at the given frame number. Skip all hooks, expect for `render_ahead` 189 + /// frames before the requested one. 190 + pub fn render_frame( 191 + &self, 192 + frame_no: usize, 193 + render_ahead: usize, 194 + ) -> Result<svg::Node> { 183 195 debug_time!("render_single_frame"); 184 196 let (tx, rx) = std::sync::mpsc::sync_channel::<EngineOutput>(2); 185 197 186 - self.render(tx, |ctx| { 198 + let render_ahead_range = frame_no.saturating_sub(render_ahead)..frame_no; 199 + 200 + self.render(tx, &move |ctx| { 187 201 if ctx.frame() == frame_no { 188 202 EngineControl::Finish 189 - } else if ctx.frame() < frame_no { 203 + } else if render_ahead_range.contains(&ctx.frame()) { 190 204 EngineControl::Walk 191 205 } else { 192 - EngineControl::Stop 206 + EngineControl::Skip 193 207 } 194 208 })?; 195 209 196 - println!("Waiting for rendered frame..."); 197 210 for output in rx.iter() { 198 211 match output { 199 212 EngineOutput::Finished => break, ··· 204 217 return Err(anyhow::format_err!( 205 218 "Renderer did not output any non-empty frames" 206 219 )); 207 - } 208 - 209 - pub fn render_everything( 210 - &self, 211 - output: SyncSender<EngineOutput>, 212 - ) -> Result<usize> { 213 - self.render(output, |_| EngineControl::Render) 214 - } 215 - 216 - pub fn render_with_overrides( 217 - &self, 218 - output: SyncSender<EngineOutput>, 219 - ) -> Result<usize> { 220 - let actual_ms_range = self.constrained_ms_range(); 221 - 222 - if actual_ms_range != self.total_ms_range() { 223 - self.progress_bars.rendering.log( 224 - "Constrained", 225 - &Timestamp::from_ms_range(&actual_ms_range).pretty(), 226 - ); 227 - } 228 - 229 - self.render(output, |ctx| { 230 - if actual_ms_range.contains(&ctx.ms) { 231 - EngineControl::Render 232 - } else if ctx.ms > actual_ms_range.end { 233 - EngineControl::Stop 234 - } else { 235 - EngineControl::Skip 236 - } 237 - }) 238 220 } 239 221 } 240 222
+2 -2
src/video/preview.html
··· 16 16 <div id="frame_monitor"></div> 17 17 <div class="controls"> 18 18 <button style="font-family: monospace" id="play_pause">|&gt;</button> 19 - <input type="range" value="0" id="requested_frame" min="1" max="300" /> 19 + <input type="range" value="0" id="requested_frame" min="1" max="%frames_count%" /> 20 20 <code id="requested_frame_number"></code> 21 21 </div> 22 22 <script type="module"> ··· 41 41 } 42 42 } 43 43 44 - requested_frame.oninput = debounce(10, ({ target }) => { 44 + requested_frame.oninput = debounce(200, ({ target }) => { 45 45 render(target.valueAsNumber) 46 46 }) 47 47
+9 -5
src/video/server.rs
··· 1 - use crate::Video; 1 + use crate::{Video, ui::Log}; 2 2 use axum::{Router, extract::Path, response::Html, routing}; 3 3 use std::sync::Arc; 4 4 ··· 12 12 pub fn new<C: 'static + Default>(video: Arc<Video<C>>) -> Self { 13 13 let _ = video.progress.clear(); 14 14 15 + let total_frames_count = video.ms_to_frames(video.total_duration_ms()); 16 + 15 17 let router = Router::new() 16 - .route("/", routing::get(async || Html(PREVIEW_HTML))) 17 - .route("/frame/{number_dot_svg}", 18 - routing::get(async move |Path(number_dot_svg): Path<String>| { 18 + .route("/", routing::get(async move || Html(PREVIEW_HTML.replace("%frames_count%", &total_frames_count.to_string())))) 19 + .route("/frame/{number_dot_svg}", routing::get(async move |Path(number_dot_svg): Path<String>| { 19 20 let number: usize = number_dot_svg 20 21 .strip_suffix(".svg") 21 22 .expect("Expecting /frame/{number}.svg, didn't find .svg at the end") ··· 25 26 println!(""); 26 27 println!("Frame number requested: {number}"); 27 28 28 - match video.render_single_frame(number) { 29 + match video.render_frame(number, 500) { 29 30 // Ok((timecode, svg)) => svg.to_string().replace( 30 31 // "</svg>", 31 32 // &format!(r#"<meta name="shapemaker:timecode" content="{timecode}" /></svg>"#) ··· 51 52 52 53 impl<C: 'static + Default> Video<C> { 53 54 pub async fn serve(self, address: &str) { 55 + self.progress_bars 56 + .loading 57 + .log_cyan("Listening", &format!("on {address}")); 54 58 VideoServer::new(Arc::new(self)).start(address).await; 55 59 } 56 60 }
+1
src/video/video.rs
··· 28 28 } 29 29 } 30 30 31 + #[derive(PartialEq, PartialOrd, Eq, Ord)] 31 32 pub struct Timestamp(pub usize); 32 33 33 34 impl Timestamp {