From 4e3dda2dc0658e2396f7e183f171ca076db4d8e9 Mon Sep 17 00:00:00 2001 From: Sven Assmann Date: Tue, 15 Feb 2022 10:26:25 +0100 Subject: [PATCH 1/4] feat(#36): Add framerate cli option - add a framerate cli argument `-f | --framerate` - limit the possible framerates to 4 and 8 for now - introduce new wrapper types for better readability - adjust the docs --- Cargo.lock | 1 + Cargo.toml | 3 +- README.md | 80 ++++++++++++------- src/capture.rs | 87 --------------------- src/capture/framerate.rs | 29 +++++++ src/capture/mod.rs | 5 ++ src/capture/processor.rs | 161 +++++++++++++++++++++++++++++++++++++++ src/cli.rs | 17 ++++- src/generators/gif.rs | 11 ++- src/main.rs | 65 +++++++++------- src/tips.rs | 1 + 11 files changed, 313 insertions(+), 147 deletions(-) delete mode 100644 src/capture.rs create mode 100644 src/capture/framerate.rs create mode 100644 src/capture/mod.rs create mode 100644 src/capture/processor.rs diff --git a/Cargo.lock b/Cargo.lock index fe1f117..cd81b25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -809,6 +809,7 @@ dependencies = [ "core-foundation", "core-foundation-sys", "core-graphics", + "crossbeam-channel", "env_logger", "humantime", "image", diff --git a/Cargo.toml b/Cargo.toml index 8919767..2f90f55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ log = "0.4.14" env_logger = "0.9.0" simplerand = "1.3.0" humantime = "2.1.0" +crossbeam-channel = "0.5" [dependencies.clap] version = "3.1.6" @@ -52,7 +53,7 @@ e2e_tests = [] section = "x11" depends = "imagemagick" extended-description = """## Features -- Screenshotting your terminal with 4 frames per second (every 250ms) +- Screenshotting your terminal with 4/8 frames per second - Generates high quality small sized animated gif images - **Build-In idle frames detection and optimization** (for super fluid presentations) diff --git a/README.md b/README.md index be0f8d7..4a824d3 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,11 @@ Blazingly fast terminal recorder that generates animated gif images for the web ![demo](./docs/demo.gif) ## Features -- Screenshotting your terminal with 4 frames per second (every 250ms) -- Generates high quality small sized animated gif images or mp4 videos +- Screenshotting your terminal with 4/8 frames per second +- Generates high-quality small-sized animated gif images or mp4 videos - **Build-In idle frames detection and optimization** (for super fluid presentations) - Applies (can be disabled) border decor effects like drop shadow -- Runs on MacOS and Linux +- Runs on macOS and Linux - Uses native efficient APIs - Runs without any cloud service and entirely offline - No issues with terminal sizes larger than 80x24 @@ -123,7 +123,7 @@ t-rec /bin/sh ### Full Options ```sh -t-rec 0.7.0 +t-rec 0.7.1 Sven Assmann Blazingly fast terminal recorder that generates animated gif images for the web written in rust. @@ -135,28 +135,56 @@ ARGS: pass it here. For example '/bin/sh' OPTIONS: - -b, --bg Background color when decors are used [default: transparent] - [possible values: white, black, transparent] - -d, --decor Decorates the animation with certain, mostly border effects - [default: none] [possible values: shadow, none] - -e, --end-pause to specify the pause time at the end of the animation, that - time the gif will show the last frame - -h, --help Print help information - -l, --ls-win If you want to see a list of windows available for recording - by their id, you can set env var 'WINDOWID' or `--win-id` to - record this specific window only - -m, --video Generates additionally to the gif a mp4 video of the recording - -M, --video-only Generates only a mp4 video and not gif - -n, --natural If you want a very natural typing experience and disable the - idle detection and sampling optimization - -q, --quiet Quiet mode, suppresses the banner: 'Press Ctrl+D to end - recording' - -s, --start-pause to specify the pause time at the start of the animation, that - time the gif will show the first frame - -v, --verbose Enable verbose insights for the curious - -V, --version Print version information - -w, --win-id Window Id (see --ls-win) that should be captured, instead of - the current terminal + -b, --bg + Background color when decors are used [default: transparent] [possible values: white, + black, transparent] + + -d, --decor + Decorates the animation with certain, mostly border effects [default: none] [possible + values: shadow, none] + + -e, --end-pause + Specify the pause time at the end of the animation, that time the gif will show the last + frame + + -f, --framerate + Increase the screen capturing rate (framerate) [default: 4] [possible values: 4, 8] + + -h, --help + Print help information + + -l, --ls-win + If you want to see a list of windows available for recording by their id, you can set + env var 'WINDOWID' or `--win-id` to record this specific window only + + -m, --video + Generates additionally to the gif a mp4 video of the recording + + -M, --video-only + Generates only a mp4 video and not gif + + -n, --natural + If you want a very natural typing experience and disable the idle detection and sampling + optimization + + -o, --output + Specify the output file (without extension) [default: t-rec] + + -q, --quiet + Quiet mode, suppresses the banner: 'Press Ctrl+D to end recording' + + -s, --start-pause + Specify the pause time at the start of the animation, that time the gif will show the + first frame + + -v, --verbose + Enable verbose insights for the curious + + -V, --version + Print version information + + -w, --win-id + Window Id (see --ls-win) that should be captured, instead of the current terminal ``` ### Disable idle detection & optimization diff --git a/src/capture.rs b/src/capture.rs deleted file mode 100644 index 01fcf3c..0000000 --- a/src/capture.rs +++ /dev/null @@ -1,87 +0,0 @@ -use anyhow::{Context, Result}; -use image::save_buffer; -use image::ColorType::Rgba8; -use std::borrow::Borrow; -use std::ops::{Add, Sub}; -use std::sync::mpsc::Receiver; -use std::sync::{Arc, Mutex}; -use std::time::{Duration, Instant}; -use tempfile::TempDir; - -use crate::utils::{file_name_for, IMG_EXT}; -use crate::{ImageOnHeap, PlatformApi, WindowId}; - -/// captures screenshots as file on disk -/// collects also the timecodes when they have been captured -/// stops once receiving something in rx -pub fn capture_thread( - rx: &Receiver<()>, - api: impl PlatformApi, - win_id: WindowId, - time_codes: Arc>>, - tempdir: Arc>, - force_natural: bool, -) -> Result<()> { - let duration = Duration::from_millis(250); - let start = Instant::now(); - let mut idle_duration = Duration::from_millis(0); - let mut last_frame: Option = None; - let mut identical_frames = 0; - let mut last_now = Instant::now(); - loop { - // blocks for a timeout - if rx.recv_timeout(duration).is_ok() { - break; - } - let now = Instant::now(); - let effective_now = now.sub(idle_duration); - let tc = effective_now.saturating_duration_since(start).as_millis(); - let image = api.capture_window_screenshot(win_id)?; - if !force_natural { - if last_frame.is_some() - && image - .samples - .as_slice() - .eq(last_frame.as_ref().unwrap().samples.as_slice()) - { - identical_frames += 1; - } else { - identical_frames = 0; - } - } - - if identical_frames > 0 { - // let's track now the duration as idle - idle_duration = idle_duration.add(now.duration_since(last_now)); - } else { - if let Err(e) = save_frame(&image, tc, tempdir.lock().unwrap().borrow(), file_name_for) - { - eprintln!("{}", &e); - return Err(e); - } - time_codes.lock().unwrap().push(tc); - last_frame = Some(image); - identical_frames = 0; - } - last_now = now; - } - - Ok(()) -} - -/// saves a frame as a tga file -pub fn save_frame( - image: &ImageOnHeap, - time_code: u128, - tempdir: &TempDir, - file_name_for: fn(&u128, &str) -> String, -) -> Result<()> { - save_buffer( - tempdir.path().join(file_name_for(&time_code, IMG_EXT)), - &image.samples, - image.layout.width, - image.layout.height, - image.color_hint.unwrap_or(Rgba8), - ) - .context("Cannot save frame") -} diff --git a/src/capture/framerate.rs b/src/capture/framerate.rs new file mode 100644 index 0000000..cface3c --- /dev/null +++ b/src/capture/framerate.rs @@ -0,0 +1,29 @@ +use std::fmt::{Display, Formatter}; + +#[derive(Clone)] +pub struct Framerate(u32); + +impl Framerate { + pub fn new(f: u32) -> Self { + Self(f) + } +} + +impl Display for Framerate { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let framerate = self.0; + write!(f, "framerate {framerate} [fps]") + } +} + +impl From for Framerate { + fn from(fr: u32) -> Self { + Self(fr) + } +} + +impl AsRef for Framerate { + fn as_ref(&self) -> &u32 { + &self.0 + } +} diff --git a/src/capture/mod.rs b/src/capture/mod.rs new file mode 100644 index 0000000..8c3042a --- /dev/null +++ b/src/capture/mod.rs @@ -0,0 +1,5 @@ +mod framerate; +mod processor; + +pub use framerate::*; +pub use processor::*; diff --git a/src/capture/processor.rs b/src/capture/processor.rs new file mode 100644 index 0000000..699ea13 --- /dev/null +++ b/src/capture/processor.rs @@ -0,0 +1,161 @@ +use anyhow::Context; +use crossbeam_channel::Receiver; +use image::save_buffer; +use image::ColorType::Rgba8; +use rayon::ThreadPoolBuilder; +use std::borrow::Borrow; +use std::ops::Sub; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; +use tempfile::TempDir; + +use crate::capture::Framerate; +use crate::utils::{file_name_for, IMG_EXT}; +use crate::{ImageOnHeap, PlatformApi, Result, WindowId}; + +#[derive(Eq, PartialEq)] +pub enum FrameDropStrategy { + DoNotDropAny, + DropIdenticalFrames, +} + +#[derive(Clone)] +struct FrameComparator<'a> { + last_frame: Option, + strategy: &'a FrameDropStrategy, +} + +impl<'a> FrameComparator<'a> { + pub fn drop_frame(&mut self, frame: ImageOnHeap) -> bool { + if self.last_frame.is_none() { + self.last_frame = Some(frame); + false + } else { + false + } + } +} + +/// captures screenshots as file on disk +/// collects also the timecodes when they have been captured +/// stops once receiving something in rx +pub fn capture_thread( + rx: &Receiver<()>, + api: impl PlatformApi + Sync, + win_id: WindowId, + time_codes: Arc>>, + tempdir: Arc, + frame_drop_strategy: &FrameDropStrategy, + framerate: &Framerate, +) -> Result<()> { + let pool = ThreadPoolBuilder::default().build()?; + let duration = Duration::from_secs(1) / *framerate.as_ref(); + let start = Instant::now(); + let mut idle_duration = Duration::from_millis(0); + let mut last_frame: Option = None; + let mut identical_frames = 0; + let mut last_time = Instant::now(); + let api = Arc::new(api); + let comp = Arc::new(FrameComparator { + last_frame: None, + strategy: frame_drop_strategy, + }); + // let rx = Arc::new(rx); + // let mut results: Arc>>> = Arc::new(Mutex::new(Vec::new())); + + pool.scope(|s| { + loop { + let delta = Instant::now().saturating_duration_since(last_time); + let sleep_time = duration.sub(delta); + // thread::sleep(sleep_time); + // blocks for a timeout + if rx.recv_timeout(sleep_time).is_ok() { + if pool.current_thread_has_pending_tasks().unwrap_or(false) { + println!( + "there is a backlog of frames that needs to be persisted, this may take a bit ...", + ); + } + return; + } + let now = Instant::now(); + let timecode = now.saturating_duration_since(start).as_millis(); + // let effective_now = now.sub(idle_duration); + let api = api.clone(); + let tempdir = tempdir.clone(); + time_codes.lock().unwrap().push(timecode); + + s.spawn(move |_| { + let frame = api.capture_window_screenshot(win_id); + + if let Ok(frame) = frame { + save_frame(&frame, timecode, tempdir.borrow(), file_name_for).unwrap(); + // results.borrow_mut().lock().unwrap().push(result); + } + }); + + /* + let image = api.capture_window_screenshot(win_id)?; + if frame_drop_strategy == &FrameDropStrategy::DropIdenticalFrames { + if last_frame.is_some() + && image + .samples + .as_slice() + .eq(last_frame.as_ref().unwrap().samples.as_slice()) + { + identical_frames += 1; + } else { + identical_frames = 0; + } + } + + if identical_frames > 0 { + // let's track now the duration as idle + idle_duration = idle_duration.add(now.duration_since(last_now)); + } else { + if let Err(e) = save_frame(&image, tc, tempdir.lock().unwrap().borrow(), file_name_for) + { + eprintln!("{}", &e); + return Err(e); + } + time_codes.lock().unwrap().push(tc); + last_frame = Some(image); + identical_frames = 0; + } + */ + + last_time = now; + } + }); + + Ok(()) +} + +fn capture_and_save_frame( + api: Arc, + win_id: WindowId, + timecode: u128, + tempdir: Arc>, + file_name_fn: fn(&u128, &str) -> String, +) -> Result<()> { + let mut result: Result<()> = Ok(()); + rayon::scope(|s| s.spawn(|_| {})); + + Ok(()) +} + +/// saves a frame as a tga file +pub fn save_frame( + image: &ImageOnHeap, + time_code: u128, + tempdir: &TempDir, + file_name_for: fn(&u128, &str) -> String, +) -> Result<()> { + save_buffer( + tempdir.path().join(file_name_for(&time_code, IMG_EXT)), + &image.samples, + image.layout.width, + image.layout.height, + image.color_hint.unwrap_or(Rgba8), + ) + .context("Cannot save frame") +} diff --git a/src/cli.rs b/src/cli.rs index 5376fa6..c6d3ece 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -91,7 +91,7 @@ pub fn launch() -> ArgMatches { .required(false) .short('e') .long("end-pause") - .help("to specify the pause time at the end of the animation, that time the gif will show the last frame"), + .help("Specify the pause time at the end of the animation, that time the gif will show the last frame"), ) .arg( Arg::new("start-pause") @@ -100,7 +100,7 @@ pub fn launch() -> ArgMatches { .required(false) .short('s') .long("start-pause") - .help("to specify the pause time at the start of the animation, that time the gif will show the first frame"), + .help("Specify the pause time at the start of the animation, that time the gif will show the first frame"), ) .arg( Arg::new("file") @@ -109,7 +109,18 @@ pub fn launch() -> ArgMatches { .short('o') .long("output") .default_value("t-rec") - .help("to specify the output file (without extension)"), + .help("Specify the output file (without extension)"), + ) + .arg( + Arg::new("framerate") + .value_name("frames per second") + .takes_value(true) + .required(false) + .short('f') + .long("framerate") + .default_value("4") + // .possible_values(&["4", "8"]) + .help("Increase the screen capturing rate (framerate)"), ) .arg( Arg::new("program") diff --git a/src/generators/gif.rs b/src/generators/gif.rs index 92f8c61..650007a 100644 --- a/src/generators/gif.rs +++ b/src/generators/gif.rs @@ -1,6 +1,6 @@ use crate::utils::{file_name_for, IMG_EXT}; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use std::ops::Div; use std::process::{Command, Output}; use std::time::Duration; @@ -38,7 +38,12 @@ pub fn generate_gif_with_convert( cmd.arg("-loop").arg("0"); let mut delay = 0; let temp = tempdir.path(); - let last_frame_i = time_codes.len() - 1; + let last_frame_i = time_codes.last(); + if last_frame_i.is_none() { + // houston we have a problem + bail!("We got no frames :("); + } + let last_frame_i = *last_frame_i.unwrap(); for (i, tc) in time_codes.iter().enumerate() { delay = *tc - delay; let frame = temp.join(file_name_for(tc, IMG_EXT)); @@ -50,7 +55,7 @@ pub fn generate_gif_with_convert( (0, Some(delay), _) => { frame_delay += delay.as_millis().div(10) as u64; } - (i, _, Some(delay)) if i == last_frame_i => { + (i, _, Some(delay)) if i as u128 == last_frame_i => { frame_delay += delay.as_millis().div(10) as u64; } (_, _, _) => {} diff --git a/src/main.rs b/src/main.rs index adfc24f..4cd8fec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,15 @@ +mod capture; mod cli; mod common; mod decor_effect; mod generators; mod tips; +mod utils; -mod capture; #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "macos")] mod macos; -mod utils; #[cfg(target_os = "windows")] mod win; @@ -27,13 +27,13 @@ use crate::decor_effect::{apply_big_sur_corner_effect, apply_shadow_effect}; use crate::generators::{check_for_gif, check_for_mp4, generate_gif, generate_mp4}; use crate::tips::show_tip; -use crate::capture::capture_thread; +use crate::capture::{capture_thread, FrameDropStrategy, Framerate}; use crate::utils::{sub_shell_thread, target_file, DEFAULT_EXT, MOVIE_EXT}; use anyhow::{bail, Context}; use clap::ArgMatches; use image::FlatSamples; use std::borrow::Borrow; -use std::sync::{mpsc, Arc, Mutex}; +use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use std::{env, thread}; use tempfile::TempDir; @@ -64,7 +64,8 @@ fn main() -> Result<()> { if args.is_present("list-windows") { return ls_win(); } - + let quiet = args.is_present("quiet"); + let verbose = args.is_present("verbose"); let program: String = { if args.is_present("program") { args.value_of("program").unwrap().to_owned() @@ -77,13 +78,22 @@ fn main() -> Result<()> { let mut api = setup()?; api.calibrate(win_id)?; - let force_natural = args.is_present("natural-mode"); + let frame_drop_strategy = args + .is_present("natural-mode") + .then(|| FrameDropStrategy::DoNotDropAny) + .unwrap_or_else(|| FrameDropStrategy::DropIdenticalFrames); let should_generate_gif = !args.is_present("video-only"); let should_generate_video = args.is_present("video") || args.is_present("video-only"); let (start_delay, end_delay) = ( parse_delay(args.value_of("start-pause"), "start-pause")?, parse_delay(args.value_of("end-pause"), "end-pause")?, ); + let framerate = args + .value_of("framerate") + .unwrap() + .parse::() + .map(|f| Framerate::new(f)) + .context("Invalid value for framerate")?; if should_generate_gif { check_for_gif()?; @@ -93,34 +103,38 @@ fn main() -> Result<()> { } // the nice thing is the cleanup on drop - let tempdir = Arc::new(Mutex::new( - TempDir::new().context("Cannot create tempdir.")?, - )); + let tempdir = Arc::new(TempDir::new().context("Cannot create tempdir.")?); let time_codes = Arc::new(Mutex::new(Vec::new())); - let (tx, rx) = mpsc::channel(); + let (tx, rx) = crossbeam_channel::bounded(1); let photograph = { let tempdir = tempdir.clone(); let time_codes = time_codes.clone(); - let force_natural = force_natural; + let framerate = framerate.clone(); thread::spawn(move || -> Result<()> { - capture_thread(&rx, api, win_id, time_codes, tempdir, force_natural) + capture_thread( + &rx, + api, + win_id, + time_codes, + tempdir, + &frame_drop_strategy, + &framerate, + ) }) }; let interact = thread::spawn(move || -> Result<()> { sub_shell_thread(&program).map(|_| ()) }); clear_screen(); - if args.is_present("verbose") { - println!( - "Frame cache dir: {:?}", - tempdir.lock().expect("Cannot lock tempdir resource").path() - ); + if verbose { + println!("Frame cache dir: {:?}", tempdir.path()); + let fr = format!(" @ {}", &framerate); if let Some(window) = window_name { - println!("Recording window: {:?}", window); + println!("Recording window: {window}{fr}"); } else { - println!("Recording window id: {}", win_id); + println!("Recording window id: {win_id}{fr}"); } } - if args.is_present("quiet") { + if quiet { println!(); } else { println!("[t-rec]: Press Ctrl+D to end recording"); @@ -145,15 +159,12 @@ fn main() -> Result<()> { ); show_tip(); - apply_big_sur_corner_effect( - &time_codes.lock().unwrap(), - tempdir.lock().unwrap().borrow(), - ); + apply_big_sur_corner_effect(&time_codes.lock().unwrap(), tempdir.borrow()); if let Some("shadow") = args.value_of("decor") { apply_shadow_effect( &time_codes.lock().unwrap(), - tempdir.lock().unwrap().borrow(), + tempdir.borrow(), args.value_of("bg").unwrap().to_string(), ) } @@ -165,7 +176,7 @@ fn main() -> Result<()> { time += prof! { generate_gif( &time_codes.lock().unwrap(), - tempdir.lock().unwrap().borrow(), + tempdir.borrow(), &format!("{}.{}", target, DEFAULT_EXT), start_delay, end_delay @@ -177,7 +188,7 @@ fn main() -> Result<()> { time += prof! { generate_mp4( &time_codes.lock().unwrap(), - tempdir.lock().unwrap().borrow(), + tempdir.borrow(), &format!("{}.{}", target, MOVIE_EXT), )?; } diff --git a/src/tips.rs b/src/tips.rs index 011154b..3343b5f 100644 --- a/src/tips.rs +++ b/src/tips.rs @@ -6,6 +6,7 @@ const TIPS: &[&str] = &[ "To add a pause at the beginning of the gif loop, use e.g. option `-s 500ms` option", "To prevent cutting out stall frames, checkout the `-n` option", "To remove the shadow around the gif, use the `-d none` option", + "To double the capturing framerate, use the option `-f 8`", "For a mp4 video, use the `-m` option", "To suppress the 'Ctrl+D' banner, use the `-q` option", ]; From cc9104f5b3d86ba31c7c761d5f72b638e6e9b675 Mon Sep 17 00:00:00 2001 From: Sven Assmann Date: Tue, 15 Feb 2022 10:28:28 +0100 Subject: [PATCH 2/4] chore: make clippy happy --- src/capture/processor.rs | 15 +-------------- src/main.rs | 4 ++-- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/capture/processor.rs b/src/capture/processor.rs index 699ea13..7f4fdea 100644 --- a/src/capture/processor.rs +++ b/src/capture/processor.rs @@ -44,7 +44,7 @@ pub fn capture_thread( api: impl PlatformApi + Sync, win_id: WindowId, time_codes: Arc>>, - tempdir: Arc, + tempdir: Arc>, frame_drop_strategy: &FrameDropStrategy, framerate: &Framerate, ) -> Result<()> { @@ -130,19 +130,6 @@ pub fn capture_thread( Ok(()) } -fn capture_and_save_frame( - api: Arc, - win_id: WindowId, - timecode: u128, - tempdir: Arc>, - file_name_fn: fn(&u128, &str) -> String, -) -> Result<()> { - let mut result: Result<()> = Ok(()); - rayon::scope(|s| s.spawn(|_| {})); - - Ok(()) -} - /// saves a frame as a tga file pub fn save_frame( image: &ImageOnHeap, diff --git a/src/main.rs b/src/main.rs index 4cd8fec..4e1e322 100644 --- a/src/main.rs +++ b/src/main.rs @@ -81,7 +81,7 @@ fn main() -> Result<()> { let frame_drop_strategy = args .is_present("natural-mode") .then(|| FrameDropStrategy::DoNotDropAny) - .unwrap_or_else(|| FrameDropStrategy::DropIdenticalFrames); + .unwrap_or(FrameDropStrategy::DropIdenticalFrames); let should_generate_gif = !args.is_present("video-only"); let should_generate_video = args.is_present("video") || args.is_present("video-only"); let (start_delay, end_delay) = ( @@ -92,7 +92,7 @@ fn main() -> Result<()> { .value_of("framerate") .unwrap() .parse::() - .map(|f| Framerate::new(f)) + .map(Framerate::new) .context("Invalid value for framerate")?; if should_generate_gif { From 39afa5405027946243fde11bb8a9e0c1db57933c Mon Sep 17 00:00:00 2001 From: Sven Assmann Date: Mon, 28 Feb 2022 09:56:23 +0100 Subject: [PATCH 3/4] feat(#36): Add framerate cli option - add a framerate cli argument `-f | --framerate` - limit the possible framerates to 4 and 8 for now - introduce new wrapper types for better readability - adjust the docs - retyping of u128 to Timecode not everywhere fully clean - the framedrop mechanism is not thread safe solid --- Cargo.lock | 7 +++ Cargo.toml | 1 + src/capture/frame.rs | 55 +++++++++++++++++++++ src/capture/mod.rs | 4 ++ src/capture/processor.rs | 101 ++++++++++++++++++--------------------- src/capture/timecode.rs | 14 ++++++ src/common/image.rs | 8 ++-- src/decor_effect.rs | 7 +-- src/generators/gif.rs | 10 ++-- src/generators/mp4.rs | 3 +- src/main.rs | 2 +- src/utils.rs | 4 +- 12 files changed, 147 insertions(+), 69 deletions(-) create mode 100644 src/capture/frame.rs create mode 100644 src/capture/timecode.rs diff --git a/Cargo.lock b/Cargo.lock index cd81b25..4011393 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "blockhash" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa8e2f31be6f788907cafda9ef15035d8a43edb20bca881a307bbc31333fe7c" + [[package]] name = "bumpalo" version = "3.9.1" @@ -805,6 +811,7 @@ name = "t-rec" version = "0.7.3" dependencies = [ "anyhow", + "blockhash", "clap", "core-foundation", "core-foundation-sys", diff --git a/Cargo.toml b/Cargo.toml index 2f90f55..cefb662 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ env_logger = "0.9.0" simplerand = "1.3.0" humantime = "2.1.0" crossbeam-channel = "0.5" +blockhash = "0.3" [dependencies.clap] version = "3.1.6" diff --git a/src/capture/frame.rs b/src/capture/frame.rs new file mode 100644 index 0000000..3ccc6d4 --- /dev/null +++ b/src/capture/frame.rs @@ -0,0 +1,55 @@ +use anyhow::Context; +pub use blockhash::Image; +use image::flat::View; +use image::ColorType::Rgba8; +use image::{save_buffer, FlatSamples, GenericImageView, Rgba}; +use tempfile::TempDir; + +use crate::capture::Timecode; +use crate::utils::IMG_EXT; +use crate::{ImageOnHeap, Result}; + +pub struct Frame(FlatSamples>); + +impl Frame { + /// saves a frame as a IMG_EXT file + pub fn save( + &self, + tc: &Timecode, + tempdir: &TempDir, + file_name_for: fn(&Timecode, &str) -> String, + ) -> Result<()> { + let image = self.as_ref(); + save_buffer( + tempdir.path().join(file_name_for(tc, IMG_EXT)), + &image.samples, + image.layout.width, + image.layout.height, + image.color_hint.unwrap_or(Rgba8), + ) + .context("Cannot save frame") + } +} + +impl Image for Frame { + fn dimensions(&self) -> (u32, u32) { + (self.0.layout.width, self.0.layout.height) + } + + fn get_pixel(&self, x: u32, y: u32) -> [u8; 4] { + let image: View<_, Rgba> = self.0.as_view().unwrap(); + image.get_pixel(x, y).0 + } +} + +impl AsRef>> for Frame { + fn as_ref(&self) -> &FlatSamples> { + &self.0 + } +} + +impl From for Frame { + fn from(img: ImageOnHeap) -> Self { + Self(*img) + } +} diff --git a/src/capture/mod.rs b/src/capture/mod.rs index 8c3042a..45765bc 100644 --- a/src/capture/mod.rs +++ b/src/capture/mod.rs @@ -1,5 +1,9 @@ +mod frame; mod framerate; mod processor; +mod timecode; +pub use frame::*; pub use framerate::*; pub use processor::*; +pub use timecode::*; diff --git a/src/capture/processor.rs b/src/capture/processor.rs index 7f4fdea..4d0fa18 100644 --- a/src/capture/processor.rs +++ b/src/capture/processor.rs @@ -1,7 +1,5 @@ -use anyhow::Context; +use blockhash::{blockhash256, Blockhash256}; use crossbeam_channel::Receiver; -use image::save_buffer; -use image::ColorType::Rgba8; use rayon::ThreadPoolBuilder; use std::borrow::Borrow; use std::ops::Sub; @@ -9,28 +7,34 @@ use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use tempfile::TempDir; -use crate::capture::Framerate; -use crate::utils::{file_name_for, IMG_EXT}; -use crate::{ImageOnHeap, PlatformApi, Result, WindowId}; +use crate::capture::{Framerate, Timecode}; +use crate::utils::file_name_for; +use crate::{Frame, PlatformApi, Result, WindowId}; -#[derive(Eq, PartialEq)] +#[derive(Eq, PartialEq, Clone)] pub enum FrameDropStrategy { DoNotDropAny, DropIdenticalFrames, } #[derive(Clone)] -struct FrameComparator<'a> { - last_frame: Option, - strategy: &'a FrameDropStrategy, +struct FrameComparator { + last_frames: Vec<(Timecode, Timecode, Blockhash256)>, + _strategy: FrameDropStrategy, } -impl<'a> FrameComparator<'a> { - pub fn drop_frame(&mut self, frame: ImageOnHeap) -> bool { - if self.last_frame.is_none() { - self.last_frame = Some(frame); - false +impl FrameComparator { + pub fn should_drop_frame(&mut self, timecode: &Timecode, frame: &Frame) -> bool { + let hash = blockhash256(frame); + if let Some((_last_time_code, _other_time_code, last_hash)) = self.last_frames.last() { + let last_eq = last_hash == &hash; + if !last_eq { + self.last_frames.pop(); + self.last_frames.push((timecode.clone(), hash)); + } + last_eq } else { + self.last_frames.push((timecode.clone(), hash)); false } } @@ -43,23 +47,23 @@ pub fn capture_thread( rx: &Receiver<()>, api: impl PlatformApi + Sync, win_id: WindowId, - time_codes: Arc>>, - tempdir: Arc>, + time_codes: Arc>>, + tempdir: Arc, frame_drop_strategy: &FrameDropStrategy, framerate: &Framerate, ) -> Result<()> { let pool = ThreadPoolBuilder::default().build()?; let duration = Duration::from_secs(1) / *framerate.as_ref(); let start = Instant::now(); - let mut idle_duration = Duration::from_millis(0); - let mut last_frame: Option = None; - let mut identical_frames = 0; + // let mut idle_duration = Duration::from_millis(0); + // let mut last_frame: Option = None; + // let mut identical_frames = 0; let mut last_time = Instant::now(); let api = Arc::new(api); - let comp = Arc::new(FrameComparator { - last_frame: None, - strategy: frame_drop_strategy, - }); + let comp = Arc::new(Mutex::new(FrameComparator { + last_frames: Vec::new(), + _strategy: frame_drop_strategy.clone(), + })); // let rx = Arc::new(rx); // let mut results: Arc>>> = Arc::new(Mutex::new(Vec::new())); @@ -72,26 +76,32 @@ pub fn capture_thread( if rx.recv_timeout(sleep_time).is_ok() { if pool.current_thread_has_pending_tasks().unwrap_or(false) { println!( - "there is a backlog of frames that needs to be persisted, this may take a bit ...", + "there is a backlog of frames that needs to be stored, this may take a bit ...", ); } return; } let now = Instant::now(); - let timecode = now.saturating_duration_since(start).as_millis(); - // let effective_now = now.sub(idle_duration); - let api = api.clone(); - let tempdir = tempdir.clone(); - time_codes.lock().unwrap().push(timecode); - - s.spawn(move |_| { - let frame = api.capture_window_screenshot(win_id); + s.spawn({ + let api = api.clone(); + let tempdir = tempdir.clone(); + let comp = comp.clone(); + let time_codes = time_codes.clone(); + move |_| { + let tc: Timecode = now.saturating_duration_since(start).as_millis().into(); - if let Ok(frame) = frame { - save_frame(&frame, timecode, tempdir.borrow(), file_name_for).unwrap(); - // results.borrow_mut().lock().unwrap().push(result); + let frame = api.capture_window_screenshot(win_id); + if let Ok(frame) = frame { + let frame: Frame = frame.into(); + if comp.lock().unwrap().should_drop_frame(&tc, &frame) { + return; + } + frame.save(&tc, tempdir.borrow(), file_name_for).unwrap(); + time_codes.lock().unwrap().push(tc); + // results.borrow_mut().lock().unwrap().push(result); + } } - }); + }); /* let image = api.capture_window_screenshot(win_id)?; @@ -129,20 +139,3 @@ pub fn capture_thread( Ok(()) } - -/// saves a frame as a tga file -pub fn save_frame( - image: &ImageOnHeap, - time_code: u128, - tempdir: &TempDir, - file_name_for: fn(&u128, &str) -> String, -) -> Result<()> { - save_buffer( - tempdir.path().join(file_name_for(&time_code, IMG_EXT)), - &image.samples, - image.layout.width, - image.layout.height, - image.color_hint.unwrap_or(Rgba8), - ) - .context("Cannot save frame") -} diff --git a/src/capture/timecode.rs b/src/capture/timecode.rs new file mode 100644 index 0000000..3eb803e --- /dev/null +++ b/src/capture/timecode.rs @@ -0,0 +1,14 @@ +#[derive(Clone, Debug)] +pub struct Timecode(u32); + +impl AsRef for Timecode { + fn as_ref(&self) -> &u32 { + &self.0 + } +} + +impl From for Timecode { + fn from(x: u128) -> Self { + Self(x as u32) + } +} diff --git a/src/common/image.rs b/src/common/image.rs index 02b55cd..fc379da 100644 --- a/src/common/image.rs +++ b/src/common/image.rs @@ -1,5 +1,5 @@ use crate::common::Margin; -use crate::{Image, ImageOnHeap, Result}; +use crate::{ImageOnHeap, Result}; use image::flat::View; use image::{imageops, GenericImageView, ImageBuffer, Rgba}; @@ -7,11 +7,11 @@ use image::{imageops, GenericImageView, ImageBuffer, Rgba}; /// specialized version of crop for [`ImageOnHeap`] and [`Margin`] /// #[cfg_attr(not(macos), allow(dead_code))] -pub fn crop(image: Image, margin: &Margin) -> Result { +pub fn crop(image: crate::Image, margin: &Margin) -> Result { let mut img2: View<_, Rgba> = image.as_view()?; let (width, height) = ( - img2.width() - (margin.left + margin.right) as u32, - img2.height() - (margin.top + margin.bottom) as u32, + img2.dimensions().0 - (margin.left + margin.right) as u32, + img2.dimensions().1 - (margin.top + margin.bottom) as u32, ); let image_cropped = imageops::crop( &mut img2, diff --git a/src/decor_effect.rs b/src/decor_effect.rs index c46c970..4bfe741 100644 --- a/src/decor_effect.rs +++ b/src/decor_effect.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use std::process::Command; +use crate::capture::Timecode; use anyhow::Context; use rayon::prelude::*; use tempfile::TempDir; @@ -18,7 +19,7 @@ use crate::Result; /// -layers merge \ /// t-rec-frame-000000251.tga /// ``` -pub fn apply_shadow_effect(time_codes: &[u128], tempdir: &TempDir, bg_color: String) { +pub fn apply_shadow_effect(time_codes: &[Timecode], tempdir: &TempDir, bg_color: String) { apply_effect( time_codes, tempdir, @@ -56,7 +57,7 @@ pub fn apply_shadow_effect(time_codes: &[u128], tempdir: &TempDir, bg_color: Str /// \) -alpha off -compose CopyOpacity -composite \ /// t-rec-frame-000000251.tga /// ``` -pub fn apply_big_sur_corner_effect(time_codes: &[u128], tempdir: &TempDir) { +pub fn apply_big_sur_corner_effect(time_codes: &[Timecode], tempdir: &TempDir) { let radius = 13; apply_effect( time_codes, @@ -96,7 +97,7 @@ pub fn apply_big_sur_corner_effect(time_codes: &[u128], tempdir: &TempDir) { /// apply a given effect (closure) to all frames /// fn apply_effect( - time_codes: &[u128], + time_codes: &[Timecode], tempdir: &TempDir, effect: Box Result<()> + Send + Sync>, ) { diff --git a/src/generators/gif.rs b/src/generators/gif.rs index 650007a..d032ff3 100644 --- a/src/generators/gif.rs +++ b/src/generators/gif.rs @@ -1,5 +1,6 @@ use crate::utils::{file_name_for, IMG_EXT}; +use crate::capture::Timecode; use anyhow::{bail, Context, Result}; use std::ops::Div; use std::process::{Command, Output}; @@ -27,7 +28,7 @@ pub fn check_for_imagemagick() -> Result { /// /// generating the final gif with help of convert pub fn generate_gif_with_convert( - time_codes: &[u128], + time_codes: &[Timecode], tempdir: &TempDir, target: &str, start_pause: Option, @@ -43,10 +44,11 @@ pub fn generate_gif_with_convert( // houston we have a problem bail!("We got no frames :("); } - let last_frame_i = *last_frame_i.unwrap(); + let last_frame_i = *last_frame_i.unwrap().as_ref(); for (i, tc) in time_codes.iter().enumerate() { + let tc = tc.as_ref(); delay = *tc - delay; - let frame = temp.join(file_name_for(tc, IMG_EXT)); + let frame = temp.join(file_name_for(&Timecode::from(*tc as u128), IMG_EXT)); if !frame.exists() { continue; } @@ -55,7 +57,7 @@ pub fn generate_gif_with_convert( (0, Some(delay), _) => { frame_delay += delay.as_millis().div(10) as u64; } - (i, _, Some(delay)) if i as u128 == last_frame_i => { + (i, _, Some(delay)) if i as u32 == last_frame_i => { frame_delay += delay.as_millis().div(10) as u64; } (_, _, _) => {} diff --git a/src/generators/mp4.rs b/src/generators/mp4.rs index 631589f..b7528f7 100644 --- a/src/generators/mp4.rs +++ b/src/generators/mp4.rs @@ -1,5 +1,6 @@ use std::process::Command; +use crate::capture::Timecode; use anyhow::{Context, Result}; use tempfile::TempDir; @@ -37,7 +38,7 @@ pub fn check_for_ffmpeg() -> Result<()> { /// /// generating the final mp4 with help of ffmpeg pub fn generate_mp4_with_ffmpeg( - _time_codes: &[u128], + _time_codes: &[Timecode], tempdir: &TempDir, target: &str, ) -> Result<()> { diff --git a/src/main.rs b/src/main.rs index 4e1e322..93f4eb6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,7 +27,7 @@ use crate::decor_effect::{apply_big_sur_corner_effect, apply_shadow_effect}; use crate::generators::{check_for_gif, check_for_mp4, generate_gif, generate_mp4}; use crate::tips::show_tip; -use crate::capture::{capture_thread, FrameDropStrategy, Framerate}; +use crate::capture::{capture_thread, Frame, FrameDropStrategy, Framerate}; use crate::utils::{sub_shell_thread, target_file, DEFAULT_EXT, MOVIE_EXT}; use anyhow::{bail, Context}; use clap::ArgMatches; diff --git a/src/utils.rs b/src/utils.rs index da81369..7b07c02 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -7,8 +7,8 @@ pub const MOVIE_EXT: &str = "mp4"; pub const IMG_EXT: &str = "bmp"; /// encapsulate the file naming convention -pub fn file_name_for(tc: &u128, ext: &str) -> String { - format!("t-rec-frame-{:09}.{}", tc, ext) +pub fn file_name_for>(tc: &T, ext: &str) -> String { + format!("t-rec-frame-{:09}.{}", tc.as_ref(), ext) } /// starts the main program and keeps interacting with the user From 4e7d9fac4a6cc25cb8cee8aab9aaf385f780252b Mon Sep 17 00:00:00 2001 From: Sven Assmann Date: Mon, 28 Mar 2022 16:39:33 +0200 Subject: [PATCH 4/4] feat(#36): Add framerate cli option - retyping of u128 to Timecode not everywhere fully clean - impl thread safe frame dropping strategy with comparing image hashes - keeping a linked list of `FrameEssences` --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/capture/frame_comparator.rs | 45 ++++++++++++++++ src/capture/frame_essence.rs | 24 +++++++++ src/capture/mod.rs | 4 ++ src/capture/processor.rs | 96 +++++++-------------------------- src/capture/timecode.rs | 2 +- src/generators/gif.rs | 2 +- src/main.rs | 1 + 9 files changed, 96 insertions(+), 82 deletions(-) create mode 100644 src/capture/frame_comparator.rs create mode 100644 src/capture/frame_essence.rs diff --git a/Cargo.lock b/Cargo.lock index 4011393..f21ccab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -808,7 +808,7 @@ dependencies = [ [[package]] name = "t-rec" -version = "0.7.3" +version = "0.7.3-alpha.1" dependencies = [ "anyhow", "blockhash", diff --git a/Cargo.toml b/Cargo.toml index cefb662..0eadfe1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "t-rec" -version = "0.7.3" +version = "0.7.3-alpha.1" authors = ["Sven Assmann "] edition = "2018" license = "GPL-3.0-only" diff --git a/src/capture/frame_comparator.rs b/src/capture/frame_comparator.rs new file mode 100644 index 0000000..d15ebba --- /dev/null +++ b/src/capture/frame_comparator.rs @@ -0,0 +1,45 @@ +use std::collections::LinkedList; + +use crate::capture::{FrameDropStrategy, FrameEssence}; + +pub struct FrameComparator { + last_frames: LinkedList, + strategy: FrameDropStrategy, +} + +impl FrameComparator { + pub fn new(strategy: FrameDropStrategy) -> Self { + Self { + last_frames: LinkedList::new(), + strategy, + } + } +} + +impl FrameComparator { + pub fn should_drop_frame(&mut self, frame_essence: FrameEssence) -> bool { + match self.strategy { + FrameDropStrategy::DoNotDropAny => false, + FrameDropStrategy::DropIdenticalFrames => { + if let Some(FrameEssence { when, what }) = self.last_frames.pop_back() { + if frame_essence.when > when && what == frame_essence.what { + // so the last frame and this one is the same... so let's drop it.. + // but add the current frame + self.last_frames.push_back(frame_essence); + true + } else { + let previous_should_drop_frame = self.should_drop_frame(frame_essence); + // restore the popped frame.. + self.last_frames.push_back(FrameEssence { when, what }); + + previous_should_drop_frame + } + } else { + self.last_frames.push_back(frame_essence); + + false + } + } + } + } +} diff --git a/src/capture/frame_essence.rs b/src/capture/frame_essence.rs new file mode 100644 index 0000000..dc9d25e --- /dev/null +++ b/src/capture/frame_essence.rs @@ -0,0 +1,24 @@ +use blockhash::{blockhash256, Blockhash256}; + +use crate::capture::{Frame, Timecode}; + +#[derive(Eq, PartialEq, Clone)] +pub enum FrameDropStrategy { + DoNotDropAny, + DropIdenticalFrames, +} + +#[derive(Eq, PartialOrd, PartialEq, Clone)] +pub struct FrameEssence { + pub(crate) when: Timecode, + pub(crate) what: Blockhash256, +} + +impl FrameEssence { + pub fn new(frame: &Frame, timecode: &Timecode) -> Self { + Self { + when: timecode.clone(), + what: blockhash256(frame), + } + } +} diff --git a/src/capture/mod.rs b/src/capture/mod.rs index 45765bc..824bdc0 100644 --- a/src/capture/mod.rs +++ b/src/capture/mod.rs @@ -1,9 +1,13 @@ mod frame; +mod frame_comparator; +mod frame_essence; mod framerate; mod processor; mod timecode; pub use frame::*; +pub use frame_comparator::*; +pub use frame_essence::*; pub use framerate::*; pub use processor::*; pub use timecode::*; diff --git a/src/capture/processor.rs b/src/capture/processor.rs index 4d0fa18..a4e0330 100644 --- a/src/capture/processor.rs +++ b/src/capture/processor.rs @@ -1,4 +1,3 @@ -use blockhash::{blockhash256, Blockhash256}; use crossbeam_channel::Receiver; use rayon::ThreadPoolBuilder; use std::borrow::Borrow; @@ -7,39 +6,12 @@ use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use tempfile::TempDir; +use crate::capture::frame_comparator::FrameComparator; +use crate::capture::frame_essence::{FrameDropStrategy, FrameEssence}; use crate::capture::{Framerate, Timecode}; use crate::utils::file_name_for; use crate::{Frame, PlatformApi, Result, WindowId}; -#[derive(Eq, PartialEq, Clone)] -pub enum FrameDropStrategy { - DoNotDropAny, - DropIdenticalFrames, -} - -#[derive(Clone)] -struct FrameComparator { - last_frames: Vec<(Timecode, Timecode, Blockhash256)>, - _strategy: FrameDropStrategy, -} - -impl FrameComparator { - pub fn should_drop_frame(&mut self, timecode: &Timecode, frame: &Frame) -> bool { - let hash = blockhash256(frame); - if let Some((_last_time_code, _other_time_code, last_hash)) = self.last_frames.last() { - let last_eq = last_hash == &hash; - if !last_eq { - self.last_frames.pop(); - self.last_frames.push((timecode.clone(), hash)); - } - last_eq - } else { - self.last_frames.push((timecode.clone(), hash)); - false - } - } -} - /// captures screenshots as file on disk /// collects also the timecodes when they have been captured /// stops once receiving something in rx @@ -55,23 +27,16 @@ pub fn capture_thread( let pool = ThreadPoolBuilder::default().build()?; let duration = Duration::from_secs(1) / *framerate.as_ref(); let start = Instant::now(); - // let mut idle_duration = Duration::from_millis(0); - // let mut last_frame: Option = None; - // let mut identical_frames = 0; let mut last_time = Instant::now(); let api = Arc::new(api); - let comp = Arc::new(Mutex::new(FrameComparator { - last_frames: Vec::new(), - _strategy: frame_drop_strategy.clone(), - })); - // let rx = Arc::new(rx); - // let mut results: Arc>>> = Arc::new(Mutex::new(Vec::new())); + let comparator = Arc::new(Mutex::new(FrameComparator::new( + frame_drop_strategy.clone(), + ))); pool.scope(|s| { loop { let delta = Instant::now().saturating_duration_since(last_time); let sleep_time = duration.sub(delta); - // thread::sleep(sleep_time); // blocks for a timeout if rx.recv_timeout(sleep_time).is_ok() { if pool.current_thread_has_pending_tasks().unwrap_or(false) { @@ -85,53 +50,28 @@ pub fn capture_thread( s.spawn({ let api = api.clone(); let tempdir = tempdir.clone(); - let comp = comp.clone(); + let comp = comparator.clone(); let time_codes = time_codes.clone(); move |_| { let tc: Timecode = now.saturating_duration_since(start).as_millis().into(); - - let frame = api.capture_window_screenshot(win_id); - if let Ok(frame) = frame { + if let Ok(frame) = api.capture_window_screenshot(win_id) { let frame: Frame = frame.into(); - if comp.lock().unwrap().should_drop_frame(&tc, &frame) { - return; + let frame_essence = FrameEssence::new(&frame, &tc); + { + let mut lock = comp.try_lock(); + if let Ok(ref mut mutex) = lock { + if mutex.should_drop_frame(frame_essence) { + return; + } + } else { + dbg!(" locking failed..."); + } } frame.save(&tc, tempdir.borrow(), file_name_for).unwrap(); time_codes.lock().unwrap().push(tc); - // results.borrow_mut().lock().unwrap().push(result); } } - }); - - /* - let image = api.capture_window_screenshot(win_id)?; - if frame_drop_strategy == &FrameDropStrategy::DropIdenticalFrames { - if last_frame.is_some() - && image - .samples - .as_slice() - .eq(last_frame.as_ref().unwrap().samples.as_slice()) - { - identical_frames += 1; - } else { - identical_frames = 0; - } - } - - if identical_frames > 0 { - // let's track now the duration as idle - idle_duration = idle_duration.add(now.duration_since(last_now)); - } else { - if let Err(e) = save_frame(&image, tc, tempdir.lock().unwrap().borrow(), file_name_for) - { - eprintln!("{}", &e); - return Err(e); - } - time_codes.lock().unwrap().push(tc); - last_frame = Some(image); - identical_frames = 0; - } - */ + }); last_time = now; } diff --git a/src/capture/timecode.rs b/src/capture/timecode.rs index 3eb803e..8ba21a8 100644 --- a/src/capture/timecode.rs +++ b/src/capture/timecode.rs @@ -1,4 +1,4 @@ -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] pub struct Timecode(u32); impl AsRef for Timecode { diff --git a/src/generators/gif.rs b/src/generators/gif.rs index d032ff3..9071f04 100644 --- a/src/generators/gif.rs +++ b/src/generators/gif.rs @@ -37,7 +37,7 @@ pub fn generate_gif_with_convert( println!("🎉 🚀 Generating {target}"); let mut cmd = Command::new(PROGRAM); cmd.arg("-loop").arg("0"); - let mut delay = 0; + let mut delay: u32 = 0; let temp = tempdir.path(); let last_frame_i = time_codes.last(); if last_frame_i.is_none() { diff --git a/src/main.rs b/src/main.rs index 93f4eb6..8f39836 100644 --- a/src/main.rs +++ b/src/main.rs @@ -171,6 +171,7 @@ fn main() -> Result<()> { let target = target_file(args.value_of("file").unwrap()); let mut time = Duration::default(); + time_codes.lock().unwrap().sort_unstable(); if should_generate_gif { time += prof! {