diff --git a/Cargo.lock b/Cargo.lock index f29a8fd..bc01293 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" @@ -807,13 +813,15 @@ dependencies = [ [[package]] name = "t-rec" -version = "0.7.3" +version = "0.7.3-alpha.1" dependencies = [ "anyhow", + "blockhash", "clap", "core-foundation", "core-foundation-sys", "core-graphics", + "crossbeam-channel", "env_logger", "humantime", "image", diff --git a/Cargo.toml b/Cargo.toml index ab8291e..91e42b8 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" @@ -26,6 +26,8 @@ log = "0.4.16" 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.9" @@ -52,7 +54,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/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/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/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..824bdc0 --- /dev/null +++ b/src/capture/mod.rs @@ -0,0 +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 new file mode 100644 index 0000000..a4e0330 --- /dev/null +++ b/src/capture/processor.rs @@ -0,0 +1,81 @@ +use crossbeam_channel::Receiver; +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::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}; + +/// 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 last_time = Instant::now(); + let api = Arc::new(api); + 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); + // 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 stored, this may take a bit ...", + ); + } + return; + } + let now = Instant::now(); + s.spawn({ + let api = api.clone(); + let tempdir = tempdir.clone(); + let comp = comparator.clone(); + let time_codes = time_codes.clone(); + move |_| { + let tc: Timecode = now.saturating_duration_since(start).as_millis().into(); + if let Ok(frame) = api.capture_window_screenshot(win_id) { + let frame: Frame = frame.into(); + 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); + } + } + }); + + last_time = now; + } + }); + + Ok(()) +} diff --git a/src/capture/timecode.rs b/src/capture/timecode.rs new file mode 100644 index 0000000..8ba21a8 --- /dev/null +++ b/src/capture/timecode.rs @@ -0,0 +1,14 @@ +#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] +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/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/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 92f8c61..9071f04 100644 --- a/src/generators/gif.rs +++ b/src/generators/gif.rs @@ -1,6 +1,7 @@ use crate::utils::{file_name_for, IMG_EXT}; -use anyhow::{Context, Result}; +use crate::capture::Timecode; +use anyhow::{bail, Context, Result}; use std::ops::Div; use std::process::{Command, Output}; use std::time::Duration; @@ -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, @@ -36,12 +37,18 @@ 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.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().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; } @@ -50,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 == 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 2027e24..faa19aa 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, Frame, 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(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(Framerate::new) + .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,27 +159,25 @@ 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(), ) } 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! { generate_gif( &time_codes.lock().unwrap(), - tempdir.lock().unwrap().borrow(), + tempdir.borrow(), &format!("{}.{}", target, DEFAULT_EXT), start_delay, end_delay @@ -177,7 +189,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", ]; 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