This repository has no description
0

Configure Feed

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

♻️ Refactor video encoding & frame rasterization (#76)

+468 -314
+8 -39
Cargo.lock
··· 2635 2635 "js-sys", 2636 2636 "log", 2637 2637 "wasm-bindgen", 2638 - "windows-core 0.62.2", 2638 + "windows-core", 2639 2639 ] 2640 2640 2641 2641 [[package]] ··· 5481 5481 [[package]] 5482 5482 name = "vgv" 5483 5483 version = "0.1.0" 5484 - source = "git+https://github.com/gwennlbh/vgvf#a9e996c8be627564c8466f945c3f4e104895b39c" 5484 + source = "git+https://github.com/gwennlbh/vgvf#96288225b98109f59ce5356edbd9024d6e74ed2b" 5485 5485 dependencies = [ 5486 5486 "anyhow", 5487 5487 "base64", ··· 5753 5753 checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" 5754 5754 dependencies = [ 5755 5755 "windows-collections", 5756 - "windows-core 0.61.2", 5756 + "windows-core", 5757 5757 "windows-future", 5758 5758 "windows-link 0.1.3", 5759 5759 "windows-numerics", ··· 5765 5765 source = "registry+https://github.com/rust-lang/crates.io-index" 5766 5766 checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" 5767 5767 dependencies = [ 5768 - "windows-core 0.61.2", 5768 + "windows-core", 5769 5769 ] 5770 5770 5771 5771 [[package]] ··· 5777 5777 "windows-implement", 5778 5778 "windows-interface", 5779 5779 "windows-link 0.1.3", 5780 - "windows-result 0.3.4", 5781 - "windows-strings 0.4.2", 5782 - ] 5783 - 5784 - [[package]] 5785 - name = "windows-core" 5786 - version = "0.62.2" 5787 - source = "registry+https://github.com/rust-lang/crates.io-index" 5788 - checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" 5789 - dependencies = [ 5790 - "windows-implement", 5791 - "windows-interface", 5792 - "windows-link 0.2.1", 5793 - "windows-result 0.4.1", 5794 - "windows-strings 0.5.1", 5780 + "windows-result", 5781 + "windows-strings", 5795 5782 ] 5796 5783 5797 5784 [[package]] ··· 5800 5787 source = "registry+https://github.com/rust-lang/crates.io-index" 5801 5788 checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" 5802 5789 dependencies = [ 5803 - "windows-core 0.61.2", 5790 + "windows-core", 5804 5791 "windows-link 0.1.3", 5805 5792 "windows-threading", 5806 5793 ] ··· 5845 5832 source = "registry+https://github.com/rust-lang/crates.io-index" 5846 5833 checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" 5847 5834 dependencies = [ 5848 - "windows-core 0.61.2", 5835 + "windows-core", 5849 5836 "windows-link 0.1.3", 5850 5837 ] 5851 5838 ··· 5859 5846 ] 5860 5847 5861 5848 [[package]] 5862 - name = "windows-result" 5863 - version = "0.4.1" 5864 - source = "registry+https://github.com/rust-lang/crates.io-index" 5865 - checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" 5866 - dependencies = [ 5867 - "windows-link 0.2.1", 5868 - ] 5869 - 5870 - [[package]] 5871 5849 name = "windows-strings" 5872 5850 version = "0.4.2" 5873 5851 source = "registry+https://github.com/rust-lang/crates.io-index" 5874 5852 checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 5875 5853 dependencies = [ 5876 5854 "windows-link 0.1.3", 5877 - ] 5878 - 5879 - [[package]] 5880 - name = "windows-strings" 5881 - version = "0.5.1" 5882 - source = "registry+https://github.com/rust-lang/crates.io-index" 5883 - checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" 5884 - dependencies = [ 5885 - "windows-link 0.2.1", 5886 5855 ] 5887 5856 5888 5857 [[package]]
+6 -10
examples/schedule-hell/src/main.rs
··· 87 87 Ok(()) 88 88 }); 89 89 90 - if args.contains("--serve") { 90 + let destination: String = args 91 + .free_from_str() 92 + .unwrap_or(String::from("schedule-hell.mp4")); 93 + 94 + if destination.starts_with("localhost:") { 91 95 video.serve("localhost:8000").await; 92 - } else if args.contains("--vgv") { 93 - video.encode_to_vgv( 94 - args.free_from_str() 95 - .unwrap_or(String::from("schedule-hell.vgv")), 96 - )?; 97 96 } else { 98 - video.encode( 99 - args.free_from_str() 100 - .unwrap_or(String::from("schedule-hell.mp4")), 101 - )?; 97 + video.encode(destination)?; 102 98 } 103 99 104 100 Ok(())
+4
src/graphics/canvas.rs
··· 340 340 .filled(Fill::Translucent(color, 0.25)), 341 341 ) 342 342 } 343 + 344 + pub fn dimensions<T: From<usize>>(&self) -> (T, T) { 345 + (self.width().into(), self.height().into()) 346 + } 343 347 }
+12 -56
src/rendering/canvas.rs
··· 1 1 use super::renderable::SVGRenderable; 2 - use crate::{graphics::canvas::Canvas, rendering::svg}; 2 + use crate::{ 3 + graphics::canvas::Canvas, 4 + rendering::{ 5 + rasterization::{ 6 + create_pixmap, pixmap_to_png_data, svg_to_usvg_tree, 7 + usvg_tree_to_pixmap, write_png_data, 8 + }, 9 + svg, 10 + }, 11 + }; 3 12 use measure_time::debug_time; 4 - use resvg::usvg; 5 - use std::sync::Arc; 6 13 7 14 impl SVGRenderable for Canvas { 8 15 fn render_to_svg( ··· 77 84 height: u32, 78 85 contents: &str, 79 86 ) -> anyhow::Result<tiny_skia::Pixmap> { 80 - let mut pixmap = self.create_pixmap(width, height); 87 + let mut pixmap = create_pixmap(width, height); 81 88 82 89 let parsed_svg = &svg_to_usvg_tree(contents, &self.fontdb)?; 83 90 84 - self.usvg_tree_to_pixmap(width, height, pixmap.as_mut(), parsed_svg); 91 + usvg_tree_to_pixmap(self.dimensions(), pixmap.as_mut(), parsed_svg); 85 92 86 93 Ok(pixmap) 87 94 } ··· 102 109 self.svg_to_pixmap(width, height, &svg_contents) 103 110 } 104 111 105 - fn usvg_tree_to_pixmap( 106 - &self, 107 - width: u32, 108 - height: u32, 109 - mut pixmap_mut: tiny_skia::PixmapMut<'_>, 110 - parsed_svg: &resvg::usvg::Tree, 111 - ) { 112 - debug_time!("usvg_tree_to_pixmap"); 113 - resvg::render( 114 - parsed_svg, 115 - tiny_skia::Transform::from_scale( 116 - width as f32 / self.width() as f32, 117 - height as f32 / self.height() as f32, 118 - ), 119 - &mut pixmap_mut, 120 - ); 121 - } 122 - 123 - fn create_pixmap(&self, width: u32, height: u32) -> tiny_skia::Pixmap { 124 - debug_time!("create_pixmap"); 125 - tiny_skia::Pixmap::new(width, height).expect("Failed to create pixmap") 126 - } 127 - 128 112 // previous_frame_at gives path to the previously rendered frame, which allows to copy on cache hits instead of having to re-write bytes again 129 113 pub fn render_to_png( 130 114 &mut self, ··· 160 144 Ok(()) 161 145 } 162 146 } 163 - 164 - fn svg_to_usvg_tree( 165 - svg: &str, 166 - fontdb: &Option<Arc<usvg::fontdb::Database>>, 167 - ) -> anyhow::Result<resvg::usvg::Tree> { 168 - debug_time!("svg_to_usvg_tree"); 169 - Ok(resvg::usvg::Tree::from_str( 170 - svg, 171 - &match fontdb { 172 - Some(fontdb) => resvg::usvg::Options { 173 - fontdb: fontdb.clone(), 174 - ..Default::default() 175 - }, 176 - None => resvg::usvg::Options::default(), 177 - }, 178 - )?) 179 - } 180 - 181 - fn pixmap_to_png_data(pixmap: tiny_skia::Pixmap) -> anyhow::Result<Vec<u8>> { 182 - debug_time!("pixmap_to_png_data"); 183 - Ok(pixmap.encode_png()?) 184 - } 185 - 186 - fn write_png_data(data: Vec<u8>, at: &str) -> anyhow::Result<()> { 187 - debug_time!("write_png_data"); 188 - std::fs::write(at, data)?; 189 - Ok(()) 190 - }
+1 -7
src/rendering/mod.rs
··· 4 4 pub mod fonts; 5 5 pub mod layer; 6 6 pub mod objects; 7 + pub mod rasterization; 7 8 pub mod renderable; 8 9 pub mod svg; 9 10 pub mod transform; 10 11 11 - use measure_time::debug_time; 12 12 pub use renderable::{CSSRenderable, SVGAttributesRenderable, SVGRenderable}; 13 - 14 - pub fn stringify_svg(element: svg::Node) -> String { 15 - debug_time!("stringify_svg"); 16 - 17 - return element.to_string(); 18 - }
+71
src/rendering/rasterization.rs
··· 1 + use anyhow::Result; 2 + use measure_time::debug_time; 3 + use resvg::usvg; 4 + use std::sync::Arc; 5 + 6 + pub fn create_pixmap(width: u32, height: u32) -> tiny_skia::Pixmap { 7 + debug_time!("create_pixmap"); 8 + tiny_skia::Pixmap::new(width, height).expect("Failed to create pixmap") 9 + } 10 + 11 + pub fn paint_svg_on_pixmap( 12 + pixmap: tiny_skia::PixmapMut<'_>, 13 + svg_contents: &str, 14 + canvas_dimensions: (usize, usize), 15 + fontdb: &Option<Arc<usvg::fontdb::Database>>, 16 + ) -> Result<()> { 17 + debug_time!("paint_svg_on_pixmap"); 18 + let parsed_svg = &svg_to_usvg_tree(svg_contents, fontdb)?; 19 + 20 + usvg_tree_to_pixmap(canvas_dimensions, pixmap, parsed_svg); 21 + 22 + Ok(()) 23 + } 24 + 25 + pub fn usvg_tree_to_pixmap( 26 + canvas_dimensions: (usize, usize), 27 + mut pixmap_mut: tiny_skia::PixmapMut<'_>, 28 + parsed_svg: &resvg::usvg::Tree, 29 + ) { 30 + debug_time!("usvg_tree_to_pixmap"); 31 + 32 + let (canvas_width, canvas_height) = canvas_dimensions; 33 + let (target_width, target_height) = (pixmap_mut.width(), pixmap_mut.height()); 34 + 35 + resvg::render( 36 + parsed_svg, 37 + tiny_skia::Transform::from_scale( 38 + target_width as f32 / canvas_width as f32, 39 + target_height as f32 / canvas_height as f32, 40 + ), 41 + &mut pixmap_mut, 42 + ); 43 + } 44 + 45 + pub fn svg_to_usvg_tree( 46 + svg: &str, 47 + fontdb: &Option<Arc<usvg::fontdb::Database>>, 48 + ) -> anyhow::Result<resvg::usvg::Tree> { 49 + debug_time!("svg_to_usvg_tree"); 50 + Ok(resvg::usvg::Tree::from_str( 51 + svg, 52 + &match fontdb { 53 + Some(fontdb) => resvg::usvg::Options { 54 + fontdb: fontdb.clone(), 55 + ..Default::default() 56 + }, 57 + None => resvg::usvg::Options::default(), 58 + }, 59 + )?) 60 + } 61 + 62 + pub fn pixmap_to_png_data(pixmap: tiny_skia::Pixmap) -> anyhow::Result<Vec<u8>> { 63 + debug_time!("pixmap_to_png_data"); 64 + Ok(pixmap.encode_png()?) 65 + } 66 + 67 + pub fn write_png_data(data: Vec<u8>, at: &str) -> anyhow::Result<()> { 68 + debug_time!("write_png_data"); 69 + std::fs::write(at, data)?; 70 + Ok(()) 71 + }
+6
src/rendering/svg.rs
··· 5 5 6 6 use crate::{Color, ColorMapping, Point, Region}; 7 7 8 + pub fn stringify_svg(element: Node) -> String { 9 + debug_time!("stringify_svg"); 10 + 11 + return element.to_string(); 12 + } 13 + 8 14 #[derive(Debug, Clone)] 9 15 pub struct Element { 10 16 pub tag: String,
+125
src/video/encoders/ffmpeg.rs
··· 1 + use crate::{ 2 + Video, 3 + rendering::rasterization::{create_pixmap, paint_svg_on_pixmap}, 4 + ui, 5 + video::{encoders::Encoder, engine::EngineOutput}, 6 + }; 7 + use anyhow::Result; 8 + use measure_time::debug_time; 9 + use std::{fs::File, io::Write, path::PathBuf, sync::Arc}; 10 + 11 + pub struct FFMpegEncoder { 12 + pixmap: tiny_skia::Pixmap, 13 + process: std::process::Child, 14 + fontdb: Option<Arc<resvg::usvg::fontdb::Database>>, 15 + destination: PathBuf, 16 + } 17 + 18 + impl<C: Default> Video<C> { 19 + pub fn setup_ffmpeg_encoder( 20 + &self, 21 + width: u32, 22 + height: u32, 23 + output_path: PathBuf, 24 + ) -> Result<FFMpegEncoder> { 25 + debug_time!("setup_encoder"); 26 + let output_path: PathBuf = output_path.into(); 27 + 28 + Ok(FFMpegEncoder { 29 + destination: output_path.clone(), 30 + fontdb: self.initial_canvas.fontdb.clone(), 31 + pixmap: create_pixmap(width, height), 32 + process: std::process::Command::new("ffmpeg") 33 + // Audio // 34 + // Take non-0 starting point into account 35 + .args(["-ss", &self.start_rendering_at.seconds_string()]) 36 + // File 37 + .args(["-i", self.audiofile.to_str().unwrap()]) 38 + // 39 + // Video // 40 + // Raw video input 41 + .args(["-f", "rawvideo"]) 42 + // RGBA Pixels 43 + .args(["-pixel_format", "rgba"]) 44 + // Dimensions 45 + .args(["-video_size", &format!("{width}x{height}")]) 46 + // FPS 47 + .args(["-framerate", &self.fps.to_string()]) 48 + // Input from pipe 49 + .args(["-i", "-"]) 50 + .stdin(std::process::Stdio::piped()) 51 + // 52 + // Mapping // 53 + // Audio from first input 54 + .args(["-map", "0:a"]) 55 + // Video from second input 56 + .args(["-map", "1:v"]) 57 + // Use shortest stream for final duration 58 + .arg("-shortest") 59 + // 60 + // Output // 61 + // Write to file 62 + .arg(output_path.to_str().unwrap()) 63 + // Debug ffmpeg too if shapemaker is debugging 64 + .args([ 65 + "-loglevel", 66 + (if log::log_enabled!(log::Level::Debug) { 67 + "debug" 68 + } else { 69 + "error" 70 + }), 71 + ]) 72 + // Put stdout/stderr here so that it doesn't mess with progress bars 73 + .stdout(File::create("ffmpeg_stdout.log")?) 74 + .stderr(File::create("ffmpeg_stderr.log")?) 75 + // 76 + // Spawn it! 77 + .spawn()?, 78 + }) 79 + } 80 + } 81 + 82 + impl Encoder for FFMpegEncoder { 83 + fn encode_frame(&mut self, output: EngineOutput) -> Result<()> { 84 + if let EngineOutput::Frame { svg, dimensions } = output { 85 + // TODO prendre width et height sur la node svg au lieu de devoir donnner un canvas initial (la grid size peut changer depuis l'initial canvas) 86 + debug_time!("encode_frame"); 87 + // Make sure that width and height are divisible by 2, as the encoder requires it 88 + 89 + // let pixmap = svg_to_pixmap(width, height, &svg.to_string())?; 90 + paint_svg_on_pixmap( 91 + self.pixmap.as_mut(), 92 + &svg.to_string(), 93 + dimensions, 94 + &self.fontdb, 95 + )?; 96 + 97 + // Send frame 98 + self.process 99 + .stdin 100 + .as_mut() 101 + .unwrap() 102 + .write_all(&self.pixmap.data())?; 103 + 104 + // let frame = 105 + // canvas.pixmap_to_hwc_frame((width as usize, height as usize), &pixmap)?; 106 + // Ok(encoder.encode(&frame, timestamp)?) 107 + Ok(()) 108 + } else { 109 + Ok(()) 110 + } 111 + } 112 + 113 + fn finish(&mut self) -> Result<()> { 114 + self.process.stdin.take().unwrap().flush().unwrap(); 115 + Ok(()) 116 + } 117 + 118 + fn finish_message(&self, time_elapsed: std::time::Duration) -> String { 119 + format!( 120 + "video to {} in {}", 121 + ui::format_filepath(&self.destination), 122 + ui::format_duration(time_elapsed) 123 + ) 124 + } 125 + }
+14
src/video/encoders/mod.rs
··· 1 + use crate::video::engine::EngineOutput; 2 + use anyhow::Result; 3 + 4 + pub mod ffmpeg; 5 + pub mod vgv; 6 + 7 + pub trait Encoder { 8 + fn encode_frame(&mut self, output: EngineOutput) -> Result<()>; 9 + fn finish(&mut self) -> Result<()>; 10 + fn finish_message(&self, time_elapsed: std::time::Duration) -> String; 11 + fn progress_message(&self, current: u64, total: u64) -> String { 12 + format!("{}/{} frames", current, total,) 13 + } 14 + }
+108
src/video/encoders/vgv.rs
··· 1 + use crate::{ 2 + Canvas, Video, 3 + rendering::svg, 4 + ui, 5 + video::{encoders::Encoder, engine::EngineOutput}, 6 + }; 7 + use ::vgv::Transcoder; 8 + use anyhow::Result; 9 + use itertools::Itertools; 10 + use std::path::PathBuf; 11 + 12 + #[derive(strum_macros::Display)] 13 + pub enum VGVTranscodeMode { 14 + ToHTML, 15 + None, 16 + } 17 + 18 + pub struct VGVEncoder { 19 + destination: PathBuf, 20 + encoder: ::vgv::Encoder, 21 + transcode: VGVTranscodeMode, 22 + } 23 + 24 + impl<C: Default> Video<C> { 25 + pub fn setup_vgv_encoder( 26 + &self, 27 + transcode: VGVTranscodeMode, 28 + width: usize, 29 + height: usize, 30 + initial_canvas: &Canvas, 31 + destination: impl Into<PathBuf>, 32 + ) -> Result<VGVEncoder> { 33 + Ok(VGVEncoder { 34 + transcode, 35 + destination: destination.into(), 36 + encoder: ::vgv::Encoder::new(::vgv::Frame::Initialization { 37 + w: width as _, 38 + h: height as _, 39 + d: (1000.0 / self.fps as f64) as _, 40 + bg: initial_canvas 41 + .background 42 + .unwrap_or_default() 43 + .render(&initial_canvas.colormap), 44 + svg: format!( 45 + r#"width={w} height={h} viewBox="-{pad} -{pad} {w} {h}""#, 46 + w = initial_canvas.width(), 47 + h = initial_canvas.height(), 48 + pad = initial_canvas.canvas_outer_padding 49 + ), 50 + }), 51 + }) 52 + } 53 + } 54 + 55 + impl Encoder for VGVEncoder { 56 + fn encode_frame(&mut self, output: EngineOutput) -> Result<()> { 57 + if let EngineOutput::Frame { ref svg, .. } = output { 58 + self.encoder.encode_svg(match svg { 59 + svg::Node::Text(text) => text.to_string(), 60 + svg::Node::SVG(svg) => svg.to_string(), 61 + svg::Node::Element(element) => element 62 + .children 63 + .iter() 64 + .map(|child| child.to_string()) 65 + .join(""), 66 + }); 67 + } 68 + 69 + Ok(()) 70 + } 71 + 72 + fn finish(&mut self) -> Result<()> { 73 + match self.transcode { 74 + VGVTranscodeMode::ToHTML => { 75 + // FIXME: not good!! 76 + let frames = self.encoder.frames.clone(); 77 + std::fs::write( 78 + self.destination.clone(), 79 + vgv::HTMLTranscoder::new() 80 + .encode(frames) 81 + .expect("Couldn't transcode VGV to HTML") 82 + .as_bytes(), 83 + )?; 84 + } 85 + 86 + VGVTranscodeMode::None => { 87 + let mut file = std::fs::File::create(self.destination.clone())?; 88 + self.encoder.dump(&mut file); 89 + } 90 + } 91 + Ok(()) 92 + } 93 + 94 + fn finish_message(&self, time_elapsed: std::time::Duration) -> String { 95 + match self.transcode { 96 + VGVTranscodeMode::None => format!( 97 + "VGV video to {} in {}", 98 + ui::format_filepath(&self.destination), 99 + ui::format_duration(time_elapsed) 100 + ), 101 + VGVTranscodeMode::ToHTML => format!( 102 + "HTML player for VGV video to {} in {}", 103 + ui::format_filepath(&self.destination), 104 + ui::format_duration(time_elapsed) 105 + ), 106 + } 107 + } 108 + }
+97 -194
src/video/encoding.rs
··· 1 1 use super::Video; 2 - use crate::rendering::svg; 3 - use crate::ui::format_duration; 2 + use crate::ui::{self, Log}; 3 + use crate::video::encoders::Encoder; 4 + use crate::video::encoders::vgv::VGVTranscodeMode; 4 5 use crate::video::engine::EngineOutput; 5 - use crate::{Canvas, ui::Log}; 6 6 use anyhow::Result; 7 - use itertools::Itertools; 8 7 use measure_time::debug_time; 9 - use std::fs::File; 10 - use std::io::Write; 8 + use std::path::PathBuf; 11 9 use std::thread; 12 - use std::{fs::create_dir_all, path::PathBuf}; 13 10 14 11 impl<AdditionalContext: Default> Video<AdditionalContext> { 15 - pub fn encode_to_vgv( 16 - &mut self, 17 - output_file: impl Into<PathBuf>, 18 - ) -> Result<()> { 19 - debug_time!("encode_to_vgv"); 20 - let output_file: PathBuf = output_file.into(); 21 - 22 - if output_file.exists() { 23 - std::fs::remove_file(&output_file)?; 24 - } 25 - 26 - create_dir_all( 27 - &output_file 28 - .parent() 29 - .expect("Given output file has no parent"), 30 - )?; 31 - 32 - let mut file = File::create(&output_file)?; 33 - 34 - let pb = self.progress_bars.encoding.clone(); 35 - pb.set_position(0); 36 - pb.set_length(self.ms_to_frames(self.duration_ms()) as _); 37 - pb.set_prefix("Encoding"); 38 - pb.set_message(format!( 39 - "with VGV, to {}{}", 40 - if output_file.is_relative() { "./" } else { "" }, 41 - output_file.to_string_lossy(), 42 - )); 43 - 44 - self.initial_canvas.load_fonts()?; 45 - let initial_canvas = self.initial_canvas.clone(); 46 - let resolution = self.resolution; 47 - let (width, height) = initial_canvas.resolution_to_size_even(resolution); 48 - 49 - let (tx, rx) = std::sync::mpsc::sync_channel::<EngineOutput>(10_000); 50 - 51 - let mut vgv_encoder = vgv::Encoder::new(vgv::Frame::Initialization { 52 - w: width as _, 53 - h: height as _, 54 - d: (1000.0 / self.fps as f64) as _, 55 - bg: initial_canvas 56 - .background 57 - .unwrap_or_default() 58 - .render(&initial_canvas.colormap), 59 - svg: format!( 60 - r#"width={w} height={h} viewBox="-{pad} -{pad} {w} {h}""#, 61 - w = initial_canvas.width(), 62 - h = initial_canvas.height(), 63 - pad = initial_canvas.canvas_outer_padding 64 - ), 65 - }); 66 - 67 - vgv_encoder.full_diff_ratio = 500; 68 - 69 - let vgv_thread = thread::spawn(move || { 70 - for output in rx.iter() { 71 - match output { 72 - EngineOutput::Finished => break, 73 - EngineOutput::Frame(ref svg) => { 74 - pb.inc(1); 75 - vgv_encoder.encode_svg(match svg { 76 - svg::Node::Text(text) => text.to_string(), 77 - svg::Node::SVG(svg) => svg.to_string(), 78 - svg::Node::Element(element) => element 79 - .children 80 - .iter() 81 - .map(|child| child.to_string()) 82 - .join(""), 83 - }); 84 - } 85 - } 86 - } 12 + pub fn encode(&mut self, output_file: impl Into<PathBuf>) -> Result<()> { 13 + debug_time!("encode"); 87 14 88 - vgv_encoder.dump(&mut file); 89 - }); 15 + let encoder = self.setup_encoder(output_file)?; 90 16 91 - self.render_with_overrides(tx)?; 92 - 93 - vgv_thread.join().expect("VGV thread panicked"); 94 - 95 - self.progress_bars.encoding.finish(); 96 - 97 - Ok(()) 17 + self.encode_with(encoder) 98 18 } 99 19 100 20 fn setup_encoder( 101 21 &mut self, 102 22 output_path: impl Into<PathBuf>, 103 - ) -> anyhow::Result<std::process::Child> { 104 - debug_time!("setup_encoder"); 105 - let output_path: PathBuf = output_path.into(); 106 - 23 + ) -> Result<Box<dyn Encoder + Send>> { 107 24 let (width, height) = 108 25 self.initial_canvas.resolution_to_size_even(self.resolution); 109 26 110 - Ok(std::process::Command::new("ffmpeg") 111 - // Audio // 112 - // Take non-0 starting point into account 113 - .args(["-ss", &self.start_rendering_at.seconds_string()]) 114 - // File 115 - .args(["-i", self.audiofile.to_str().unwrap()]) 116 - // 117 - // Video // 118 - // Raw video input 119 - .args(["-f", "rawvideo"]) 120 - // RGBA Pixels 121 - .args(["-pixel_format", "rgba"]) 122 - // Dimensions 123 - .args(["-video_size", &format!("{width}x{height}")]) 124 - // FPS 125 - .args(["-framerate", &self.fps.to_string()]) 126 - // Input from pipe 127 - .args(["-i", "-"]) 128 - .stdin(std::process::Stdio::piped()) 129 - // 130 - // Mapping // 131 - // Audio from first input 132 - .args(["-map", "0:a"]) 133 - // Video from second input 134 - .args(["-map", "1:v"]) 135 - // Use shortest stream for final duration 136 - .arg("-shortest") 137 - // 138 - // Output // 139 - // Write to file 140 - .arg(output_path.to_str().unwrap()) 141 - // Debug ffmpeg too if shapemaker is debugging 142 - .args([ 143 - "-loglevel", 144 - (if log::log_enabled!(log::Level::Debug) { 145 - "debug" 146 - } else { 147 - "error" 148 - }), 149 - ]) 150 - // Put stdout/stderr here so that it doesn't mess with progress bars 151 - .stdout(File::create("ffmpeg_stdout.log")?) 152 - .stderr(File::create("ffmpeg_stderr.log")?) 153 - // 154 - // Spawn it! 155 - .spawn()?) 156 - } 27 + let destination = output_path.into(); 157 28 158 - pub fn encode(&mut self, output_file: impl Into<PathBuf>) -> Result<()> { 159 - debug_time!("encode"); 160 - 161 - self.progress.remove(&self.progress_bars.loading); 162 - 163 - let output_file: PathBuf = output_file.into(); 164 - 165 - if output_file.exists() { 166 - std::fs::remove_file(&output_file)?; 29 + if destination.exists() { 30 + std::fs::remove_file(&destination)?; 167 31 } 168 32 169 - create_dir_all( 170 - &output_file 33 + std::fs::create_dir_all( 34 + &destination 171 35 .parent() 172 36 .expect("Given output file has no parent"), 173 37 )?; 174 38 175 - let mut encoder = self.setup_encoder(&output_file)?; 39 + Ok(match destination.full_extension() { 40 + ".vgv.html" => { 41 + self.progress_bars.encoding.log( 42 + "Selecting", 43 + &format!( 44 + "VGV encoder with HTML transcoding as {} ends with .vgv.html", 45 + ui::format_filepath(&destination), 46 + ), 47 + ); 48 + 49 + Box::new(self.setup_vgv_encoder( 50 + VGVTranscodeMode::ToHTML, 51 + width as _, 52 + height as _, 53 + &self.initial_canvas, 54 + destination, 55 + )?) 56 + } 57 + ".vgv" => { 58 + self.progress_bars.encoding.log( 59 + "Selecting", 60 + &format!( 61 + "VGV encoder as {} ends with .vgv (use .vgv.html for HTML transcoding)", 62 + ui::format_filepath(&destination), 63 + ), 64 + ); 65 + 66 + Box::new(self.setup_vgv_encoder( 67 + VGVTranscodeMode::None, 68 + width as _, 69 + height as _, 70 + &self.initial_canvas, 71 + destination, 72 + )?) 73 + } 74 + _ => { 75 + self.progress_bars 76 + .encoding 77 + .log("Selecting", &format!( 78 + "FFMpeg encoder, as {} doesn't end with .vgv or .vgv.html", 79 + ui::format_filepath(&destination), 80 + )); 81 + self.initial_canvas.load_fonts()?; 82 + Box::new(self.setup_ffmpeg_encoder(width, height, destination)?) 83 + } 84 + }) 85 + } 86 + 87 + pub fn encode_with( 88 + &mut self, 89 + mut encoder: Box<dyn Encoder + Send>, 90 + ) -> Result<()> { 91 + debug_time!("encode_with"); 92 + 93 + self.progress.remove(&self.progress_bars.loading); 176 94 177 95 let pb = self.progress_bars.encoding.clone(); 178 96 179 97 pb.set_length(self.ms_to_frames(self.duration_ms()) as _); 180 98 pb.set_message(""); 181 - 182 - self.initial_canvas.load_fonts()?; 183 - let initial_canvas = self.initial_canvas.clone(); 184 - let resolution = self.resolution; 185 99 186 100 let (tx, rx) = std::sync::mpsc::sync_channel::<EngineOutput>(1_000); 187 101 ··· 189 103 for output in rx.iter() { 190 104 match output { 191 105 EngineOutput::Finished => break, 192 - EngineOutput::Frame(svg) => { 106 + EngineOutput::Frame { .. } => { 193 107 pb.inc(1); 194 - pb.set_message(format!( 195 - "{}/{} frames", 108 + pb.set_message(encoder.progress_message( 196 109 pb.position(), 197 - pb.length().unwrap() 110 + pb.length().unwrap(), 198 111 )); 199 - encode_frame( 200 - &mut encoder, 201 - resolution, 202 - &initial_canvas, 203 - svg, 204 - ) 205 - .unwrap(); 206 112 } 207 113 } 114 + 115 + encoder.encode_frame(output).expect("Couldn't encode frame"); 208 116 } 209 117 210 - encoder.stdin.take().unwrap().flush().unwrap(); 118 + let finish_message = encoder.finish_message(pb.elapsed()); 119 + 120 + encoder.finish().expect("Couldn't finish encoding"); 121 + 122 + pb.finish(); 123 + pb.log("Encoded", &finish_message); 211 124 }); 212 125 213 126 self.render_with_overrides(tx)?; 214 127 215 128 encoder_thread.join().expect("Encoder thread panicked"); 216 129 217 - self.progress_bars.encoding.finish(); 218 - self.progress_bars.encoding.log( 219 - "Encoded", 220 - &format!( 221 - "video to {}{} in {}", 222 - if output_file.is_relative() { "./" } else { "" }, 223 - output_file.to_string_lossy(), 224 - format_duration(self.progress_bars.encoding.elapsed()) 225 - ), 226 - ); 227 - 228 130 self.progress.clear().unwrap(); 229 131 230 132 Ok(()) ··· 238 140 } 239 141 } 240 142 241 - fn encode_frame( 242 - encoder: &mut std::process::Child, 243 - resolution: u32, 244 - canvas: &Canvas, 245 - svg: svg::Node, 246 - ) -> anyhow::Result<()> { 247 - debug_time!("encode_frame"); 248 - // Make sure that width and height are divisible by 2, as the encoder requires it 249 - let (width, height) = canvas.resolution_to_size_even(resolution); 143 + // Because .extension() sucks 250 144 251 - let pixmap = canvas.svg_to_pixmap(width, height, &svg.to_string())?; 252 - // Send frame 253 - encoder.stdin.as_mut().unwrap().write_all(&pixmap.data())?; 145 + trait FullExtension { 146 + fn full_extension(&self) -> &str; 147 + } 254 148 255 - // let frame = 256 - // canvas.pixmap_to_hwc_frame((width as usize, height as usize), &pixmap)?; 257 - // Ok(encoder.encode(&frame, timestamp)?) 258 - Ok(()) 149 + impl FullExtension for PathBuf { 150 + fn full_extension(&self) -> &str { 151 + let filename = self 152 + .file_name() 153 + .and_then(|f| f.to_str()) 154 + .unwrap_or_default(); 155 + let parts: Vec<&str> = filename.split('.').collect(); 156 + if parts.len() <= 1 { 157 + "" 158 + } else { 159 + &filename[filename.find('.').unwrap()..] 160 + } 161 + } 259 162 }
+14 -8
src/video/engine.rs
··· 9 9 /// What data is sent to the output by the rendering engine for each rendered frame 10 10 pub enum EngineOutput { 11 11 Finished, 12 - Frame(svg::Node), 12 + Frame { 13 + svg: svg::Node, 14 + dimensions: (usize, usize), 15 + }, 13 16 } 14 17 15 18 pub struct EngineProgression { ··· 139 142 } 140 143 141 144 if !skip_rendering && context.frame() != previous_rendered_frame { 142 - output.send(EngineOutput::Frame(canvas.render_to_svg( 143 - canvas.colormap.clone(), 144 - canvas.cell_size, 145 - canvas.object_sizes, 146 - "", 147 - )?))?; 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 154 149 155 context.rendered_frames += 1; 150 156 ··· 191 197 for output in rx.iter() { 192 198 match output { 193 199 EngineOutput::Finished => break, 194 - EngineOutput::Frame(svg) => return Ok(svg), 200 + EngineOutput::Frame { svg, .. } => return Ok(svg), 195 201 } 196 202 } 197 203
+2
src/video/mod.rs
··· 7 7 8 8 #[cfg(feature = "video")] 9 9 pub mod encoding; 10 + #[cfg(feature = "video")] 11 + pub mod encoders; 10 12 11 13 #[cfg(feature = "video-server")] 12 14 pub mod server;