diff --git a/Cargo.lock b/Cargo.lock index f29a8fd..9453123 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,110 @@ version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" +[[package]] +name = "async-channel" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "once_cell", + "slab", +] + +[[package]] +name = "async-fs" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b3ca4f8ff117c37c278a2f7415ce9be55560b846b5bc4412aaa5d29c1c3dae2" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b" +dependencies = [ + "concurrent-queue", + "futures-lite", + "libc", + "log", + "once_cell", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "winapi", +] + +[[package]] +name = "async-lock" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-net" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5373304df79b9b4395068fb080369ec7178608827306ce4d081cba51cac551df" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2c06e30a24e8c78a3987d07f0930edf76ef35e027e7bdb063fccafdad1f60c" +dependencies = [ + "async-io", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "libc", + "once_cell", + "signal-hook", + "winapi", +] + +[[package]] +name = "async-task" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9" + +[[package]] +name = "atomic-waker" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" + [[package]] name = "atty" version = "0.2.14" @@ -64,6 +168,20 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "blocking" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" +dependencies = [ + "async-channel", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", + "once_cell", +] + [[package]] name = "bumpalo" version = "3.9.1" @@ -82,6 +200,12 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "cache-padded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" + [[package]] name = "cc" version = "1.0.73" @@ -125,6 +249,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -247,6 +380,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "event-listener" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" + [[package]] name = "exr" version = "1.4.1" @@ -318,6 +457,27 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +[[package]] +name = "futures-io" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "futures-sink" version = "0.3.21" @@ -637,12 +797,24 @@ dependencies = [ "objc", ] +[[package]] +name = "once_cell" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" + [[package]] name = "os_str_bytes" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + [[package]] name = "pin-project" version = "1.0.10" @@ -663,6 +835,12 @@ dependencies = [ "syn", ] +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + [[package]] name = "png" version = "0.17.5" @@ -675,6 +853,19 @@ dependencies = [ "miniz_oxide 0.5.1", ] +[[package]] +name = "polling" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" +dependencies = [ + "cfg-if", + "libc", + "log", + "wepoll-ffi", + "winapi", +] + [[package]] name = "proc-macro2" version = "1.0.36" @@ -764,6 +955,25 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "signal-hook" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + [[package]] name = "simplerand" version = "1.3.0" @@ -773,12 +983,46 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "slab" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" + [[package]] name = "smallvec" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +[[package]] +name = "smol" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cf3b5351f3e783c1d79ab5fc604eeed8b8ae9abd36b166e8b87a089efd85e4" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "spin" version = "0.9.2" @@ -815,6 +1059,7 @@ dependencies = [ "core-foundation-sys", "core-graphics", "env_logger", + "flume", "humantime", "image", "log", @@ -822,6 +1067,7 @@ dependencies = [ "objc_id", "rayon", "simplerand", + "smol", "tempfile", "x11rb", ] @@ -881,6 +1127,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" @@ -947,6 +1199,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e" +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index ab8291e..0a5de55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,8 @@ log = "0.4.16" env_logger = "0.9.0" simplerand = "1.3.0" humantime = "2.1.0" +smol = "1.2.5" +flume = { version = "0.10", default-features = false } [dependencies.clap] version = "3.1.9" @@ -71,4 +73,4 @@ presentations) [package.metadata.rpm.targets] buildflags = ["--release"] -t-rec = { path = "/usr/bin/t-rec" } \ No newline at end of file +t-rec = { path = "/usr/bin/t-rec" } diff --git a/src/capture.rs b/src/capture.rs index 01fcf3c..3fd0411 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -1,15 +1,17 @@ -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 anyhow::{Context, Result}; +use image::save_buffer; +use image::ColorType::Rgba8; +use smol::stream::block_on; use tempfile::TempDir; +use crate::common::{Frame, Recorder}; use crate::utils::{file_name_for, IMG_EXT}; -use crate::{ImageOnHeap, PlatformApi, WindowId}; +use crate::{Image, PlatformApi, WindowId}; /// captures screenshots as file on disk /// collects also the timecodes when they have been captured @@ -22,60 +24,49 @@ pub fn capture_thread( 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 recorder = Recorder::new(api, win_id, 4); + 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)?; + let start = Instant::now(); + + for frame in block_on(recorder) { 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; + let image: &Image = frame.as_ref(); + if let Some(last_image) = last_frame.as_ref() { + let last_image: &Image = last_image.as_ref(); + if image.samples.as_slice().eq(last_image.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); - } + if identical_frames == 0 { + let tc: &Instant = frame.as_ref(); + let tc = tc.duration_since(start).as_millis(); + save_image(&frame, tc, tempdir.lock().unwrap().borrow(), file_name_for)?; time_codes.lock().unwrap().push(tc); - last_frame = Some(image); - identical_frames = 0; + last_frame = Some(frame); + } + + // when there is a message we should just stop + if rx.recv_timeout(Duration::from_millis(1)).is_ok() { + break; } - last_now = now; } Ok(()) } /// saves a frame as a tga file -pub fn save_frame( - image: &ImageOnHeap, +pub fn save_image( + image: impl AsRef, time_code: u128, tempdir: &TempDir, file_name_for: fn(&u128, &str) -> String, ) -> Result<()> { + let image = image.as_ref(); save_buffer( tempdir.path().join(file_name_for(&time_code, IMG_EXT)), &image.samples, diff --git a/src/common/frame.rs b/src/common/frame.rs new file mode 100644 index 0000000..8da13a8 --- /dev/null +++ b/src/common/frame.rs @@ -0,0 +1,58 @@ +use std::time::Instant; + +use image::flat::SampleLayout; +use image::{ColorType, FlatSamples}; + +use crate::common::image::{convert_bgra_to_rgba, crop, CropMut}; +use crate::{Image, Margin, Result}; + +pub struct Frame { + image: Image, + timecode: Instant, +} + +impl Frame { + pub fn from_bgra(raw_data: Vec, channels: u8, width: u32, height: u32) -> Self { + let timecode = Instant::now(); + let mut raw_data = raw_data; + convert_bgra_to_rgba(&mut raw_data[..]); + + let color = ColorType::Rgba8; + let image = FlatSamples { + samples: raw_data, + layout: SampleLayout::row_major_packed(channels, width, height), + color_hint: Some(color), + }; + + Self { image, timecode } + } +} + +impl AsRef for Frame { + fn as_ref(&self) -> &Image { + &self.image + } +} + +impl CropMut for Frame { + fn crop(&mut self, margin: &Margin) -> Result<()> { + self.image = crop(&self, margin)?; + + Ok(()) + } +} + +impl AsRef for Frame { + fn as_ref(&self) -> &Instant { + &self.timecode + } +} + +impl From>> for Frame { + fn from(image: FlatSamples>) -> Self { + Self { + image, + timecode: Instant::now(), + } + } +} diff --git a/src/common/identify_transparency.rs b/src/common/identify_transparency.rs index 383f01f..588e2e7 100644 --- a/src/common/identify_transparency.rs +++ b/src/common/identify_transparency.rs @@ -6,8 +6,8 @@ use image::{GenericImageView, Rgba}; /// this helps to identify outer transparent regions /// since some backends provides transparency either from a compositor effect like drop shadow on ubuntu / GNOME /// or some strange right side strip on MacOS -pub fn identify_transparency(image: Image) -> Result> { - let image: View<_, Rgba> = image.as_view()?; +pub fn identify_transparency(image: impl AsRef) -> Result> { + let image: View<_, Rgba> = image.as_ref().as_view()?; let (width, height) = image.dimensions(); let half_width = width / 2; let half_height = height / 2; @@ -58,6 +58,7 @@ pub fn identify_transparency(image: Image) -> Result> { #[cfg(test)] mod test { use super::*; + use crate::common::Frame; #[test] fn should_identify_macos_right_side_issue() -> Result<()> { @@ -72,8 +73,8 @@ mod test { assert_eq!(blue, &0, "the test image is not as expected"); // when - let image_raw = image.into_flat_samples(); - let margin = identify_transparency(image_raw)?; + let image_raw: Frame = image.into_flat_samples().into(); + let margin = identify_transparency(&image_raw)?; // then assert_eq!(margin, Some(Margin::new(0, 14, 0, 0))); diff --git a/src/common/image.rs b/src/common/image.rs index 02b55cd..99b4615 100644 --- a/src/common/image.rs +++ b/src/common/image.rs @@ -1,14 +1,19 @@ -use crate::common::Margin; -use crate::{Image, ImageOnHeap, Result}; use image::flat::View; use image::{imageops, GenericImageView, ImageBuffer, Rgba}; +use crate::common::Margin; +use crate::{Image, ImageOnHeap, Result}; + +pub trait CropMut { + fn crop(&mut self, margin: &Margin) -> Result<()>; +} + /// /// specialized version of crop for [`ImageOnHeap`] and [`Margin`] /// #[cfg_attr(not(macos), allow(dead_code))] -pub fn crop(image: Image, margin: &Margin) -> Result { - let mut img2: View<_, Rgba> = image.as_view()?; +pub fn crop(image: impl AsRef, margin: &Margin) -> Result { + let mut img2: View<_, Rgba> = image.as_ref().as_view()?; let (width, height) = ( img2.width() - (margin.left + margin.right) as u32, img2.height() - (margin.top + margin.bottom) as u32, @@ -28,7 +33,7 @@ pub fn crop(image: Image, margin: &Margin) -> Result { } } - Ok(Box::new(buf.into_flat_samples())) + Ok(buf.into_flat_samples()) } /// @@ -40,19 +45,22 @@ pub fn convert_bgra_to_rgba(buffer: &mut [u8]) { #[cfg(test)] mod tests { - use super::*; + use crate::common::Frame; use image::open; + use super::*; + #[test] fn should_crop() -> Result<()> { // given let image_org = open("tests/frames/frame-macos-right-side-issue.tga")?; let image = image_org.into_rgba8(); - let image_raw = ImageOnHeap::new(image.into_flat_samples()); + let image_raw = image.into_flat_samples(); let (width, height) = (image_raw.layout.width, image_raw.layout.height); + let frame: Frame = image_raw.into(); // when - let cropped = crop(*image_raw, &Margin::new(1, 1, 1, 1))?; + let cropped = crop(&frame, &Margin::new(1, 1, 1, 1))?; // then assert_eq!(cropped.layout.width, width - 2); diff --git a/src/common/margin.rs b/src/common/margin.rs new file mode 100644 index 0000000..ace8636 --- /dev/null +++ b/src/common/margin.rs @@ -0,0 +1,66 @@ +#[derive(Debug, PartialEq)] +pub struct Margin { + pub top: u16, + pub right: u16, + pub bottom: u16, + pub left: u16, +} + +impl Margin { + pub fn new(top: u16, right: u16, bottom: u16, left: u16) -> Self { + Self { + top, + right, + bottom, + left, + } + } + + pub fn new_equal(margin: u16) -> Self { + Self::new(margin, margin, margin, margin) + } + + pub fn zero() -> Self { + Self::new_equal(0) + } + + pub fn is_zero(&self) -> bool { + self.top == 0 + && self.right == self.left + && self.left == self.bottom + && self.bottom == self.top + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn margin_new() { + let m = Margin::new(1, 2, 3, 4); + assert_eq!(m.top, 1); + assert_eq!(m.right, 2); + assert_eq!(m.bottom, 3); + assert_eq!(m.left, 4); + } + + #[test] + fn margin_new_equal() { + let m = Margin::new_equal(1); + assert_eq!(m.top, 1); + assert_eq!(m.right, 1); + assert_eq!(m.bottom, 1); + assert_eq!(m.left, 1); + } + + #[test] + fn margin_zero() { + let m = Margin::zero(); + assert_eq!(m.top, 0); + assert_eq!(m.right, 0); + assert_eq!(m.bottom, 0); + assert_eq!(m.left, 0); + assert!(m.is_zero()); + } +} diff --git a/src/common/mod.rs b/src/common/mod.rs index e669778..7591f43 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -2,81 +2,12 @@ pub mod identify_transparency; pub mod image; pub mod utils; -use crate::{ImageOnHeap, Result, WindowId, WindowList}; - -pub trait PlatformApi: Send { - /// 1. it does check for the screenshot - /// 2. it checks for transparent margins and configures the api - /// to cut them away in further screenshots - fn calibrate(&mut self, window_id: WindowId) -> Result<()>; - fn window_list(&self) -> Result; - fn capture_window_screenshot(&self, window_id: WindowId) -> Result; - fn get_active_window(&self) -> Result; -} - -#[derive(Debug, PartialEq)] -pub struct Margin { - pub top: u16, - pub right: u16, - pub bottom: u16, - pub left: u16, -} - -impl Margin { - pub fn new(top: u16, right: u16, bottom: u16, left: u16) -> Self { - Self { - top, - right, - bottom, - left, - } - } - - pub fn new_equal(margin: u16) -> Self { - Self::new(margin, margin, margin, margin) - } - - pub fn zero() -> Self { - Self::new_equal(0) - } - - pub fn is_zero(&self) -> bool { - self.top == 0 - && self.right == self.left - && self.left == self.bottom - && self.bottom == self.top - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn margin_new() { - let m = Margin::new(1, 2, 3, 4); - assert_eq!(m.top, 1); - assert_eq!(m.right, 2); - assert_eq!(m.bottom, 3); - assert_eq!(m.left, 4); - } - - #[test] - fn margin_new_equal() { - let m = Margin::new_equal(1); - assert_eq!(m.top, 1); - assert_eq!(m.right, 1); - assert_eq!(m.bottom, 1); - assert_eq!(m.left, 1); - } - - #[test] - fn margin_zero() { - let m = Margin::zero(); - assert_eq!(m.top, 0); - assert_eq!(m.right, 0); - assert_eq!(m.bottom, 0); - assert_eq!(m.left, 0); - assert!(m.is_zero()); - } -} +mod frame; +mod margin; +mod platform_api; +mod recorder; + +pub use frame::*; +pub use margin::*; +pub use platform_api::*; +pub use recorder::*; diff --git a/src/common/platform_api.rs b/src/common/platform_api.rs new file mode 100644 index 0000000..618dac1 --- /dev/null +++ b/src/common/platform_api.rs @@ -0,0 +1,12 @@ +use crate::common::Frame; +use crate::{Result, WindowId, WindowList}; + +pub trait PlatformApi: Send + Unpin + Sized { + /// 1. it does check for the screenshot + /// 2. it checks for transparent margins and configures the api + /// to cut them away in further screenshots + fn calibrate(&mut self, window_id: WindowId) -> Result<()>; + fn window_list(&self) -> Result; + fn capture_window_screenshot(&self, window_id: WindowId) -> Result; + fn get_active_window(&self) -> Result; +} diff --git a/src/common/recorder.rs b/src/common/recorder.rs new file mode 100644 index 0000000..93c4366 --- /dev/null +++ b/src/common/recorder.rs @@ -0,0 +1,93 @@ +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::Duration; + +use smol::future::FutureExt; +use smol::stream::Stream; +use smol::Timer; + +use crate::common::Frame; +use crate::{PlatformApi, WindowId}; + +pub struct Recorder { + api: A, + window_id: WindowId, + timer: Timer, +} + +impl Recorder { + pub fn new(api: A, window_id: WindowId, fps: u8) -> Self { + let fps = Duration::from_millis(1000 / fps as u64); + Self { + api, + window_id, + timer: Timer::interval(fps), + } + } +} + +impl Stream for Recorder { + type Item = Frame; + + fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + match this.timer.poll(_cx) { + Poll::Ready(_) => Poll::Ready(this.api.capture_window_screenshot(this.window_id).ok()), + Poll::Pending => Poll::Pending, + } + } +} + +#[cfg(test)] +#[cfg(target_os = "macos")] +#[cfg(feature = "e2e_tests")] +mod tests { + use smol::stream::block_on; + + use crate::macos::*; + + use super::*; + + #[test] + fn should_record_not() { + let win = 9123; + let mut api = setup().unwrap(); + api.calibrate(win).unwrap(); + let rec = Recorder::new(api, win, 10); + + let mut i = 0; + for _img in block_on(rec) { + i += 1; + if i >= 5 { + println!("Done with testing.."); + break; + } + } + } + + // #[test] + // fn should_queue_frames_for_saving() { + // let win = 9123; + // let mut api = setup().unwrap(); + // api.calibrate(win).unwrap(); + // let mut rec = Recorder::new(api, win, 10); + // + // let future = rec.next(); + // { + // let (sender, receiver) = flume::unbounded(); + // + // // A function that schedules the task when it gets woken up. + // let schedule = move |runnable| sender.send(runnable).unwrap(); + // + // // Construct a task. + // let (runnable, task) = async_task::spawn(future, schedule); + // + // // Push the task into the queue by invoking its schedule function. + // runnable.schedule(); + // + // for runnable in receiver { + // runnable.run(); + // } + // } + // } +} diff --git a/src/linux/x11_api.rs b/src/linux/x11_api.rs index c04ed67..3db51c9 100644 --- a/src/linux/x11_api.rs +++ b/src/linux/x11_api.rs @@ -1,10 +1,9 @@ use crate::common::identify_transparency::identify_transparency; use crate::common::image::convert_bgra_to_rgba; -use crate::{ImageOnHeap, Margin, PlatformApi, Result, WindowId, WindowList}; +use crate::{Margin, PlatformApi, Result, WindowId, WindowList}; +use crate::common::Frame; use anyhow::Context; -use image::flat::SampleLayout; -use image::{ColorType, FlatSamples}; use log::debug; use x11rb::connection::Connection; use x11rb::protocol::xproto::*; @@ -117,11 +116,11 @@ impl X11Api { self.get_all_sub_windows(&(screen.root as WindowId)) } - pub fn get_window_geometry(&self, window: &WindowId) -> Result<(i16, i16, u16, u16)> { + pub fn get_window_geometry(&self, window: &WindowId) -> Result<(i32, i32, u32, u32)> { let conn = &self.conn; let window = *window as Window; let geom = conn.get_geometry(window)?.reply()?; - Ok((geom.x, geom.y, geom.width, geom.height)) + Ok((geom.x as _, geom.y as _, geom.width as _, geom.height as _)) } } @@ -131,7 +130,7 @@ impl PlatformApi for X11Api { /// to cut them away in further screenshots fn calibrate(&mut self, window_id: WindowId) -> Result<()> { let image = self.capture_window_screenshot(window_id)?; - self.margin = identify_transparency(*image)?; + self.margin = identify_transparency(&image)?; Ok(()) } @@ -153,15 +152,15 @@ impl PlatformApi for X11Api { Ok(wins) } - fn capture_window_screenshot(&self, window_id: WindowId) -> Result { + fn capture_window_screenshot(&self, window_id: WindowId) -> Result { let (_, _, mut width, mut height) = self.get_window_geometry(&window_id)?; - let (mut x, mut y) = (0_i16, 0_i16); + let (mut x, mut y) = (0_i32, 0_i32); if let Some(margin) = self.margin.as_ref() { if !margin.is_zero() { - width -= margin.left + margin.right; - height -= margin.top + margin.bottom; - x = margin.left as i16; - y = margin.top as i16; + width -= (margin.left + margin.right) as u32; + height -= (margin.top + margin.bottom) as u32; + x = margin.left as _; + y = margin.top as _; } } let image = self @@ -170,10 +169,10 @@ impl PlatformApi for X11Api { .get_image( ImageFormat::Z_PIXMAP, window_id as Drawable, - x, - y, - width, - height, + x as _, + y as _, + width as _, + height as _, !0, )? .reply() @@ -182,24 +181,17 @@ impl PlatformApi for X11Api { window_id ))?; - let mut raw_data = image.data; - convert_bgra_to_rgba(&mut raw_data); - - let color = ColorType::Rgba8; - let channels = 4; - let mut buffer = FlatSamples { - samples: raw_data, - layout: SampleLayout::row_major_packed(channels, width as u32, height as u32), - color_hint: Some(color), - }; + let mut samples = image.data; + convert_bgra_to_rgba(&mut samples); if image.depth == 24 { + let stride = 3; // NOTE: in this case the alpha channel is 0, but should be set to 0xff // the index into the alpha channel - let mut i = 3; - let len = buffer.samples.len(); + let mut i = stride; + let len = samples.len(); while i < len { - let alpha = buffer.samples.get_mut(i).unwrap(); + let alpha = samples.get_mut(i).unwrap(); if alpha == &0 { *alpha = 0xff; } else { @@ -208,27 +200,36 @@ impl PlatformApi for X11Api { } // going one pixel further, still pointing to the alpha channel - i += buffer.layout.width_stride; + i += stride; } } if self.margin.is_some() { + let stride = 3; // once first image is captured, we make sure that transparency is removed // even in cases where `margin.is_zero()` let mut i = 3; - let len = buffer.samples.len(); + let len = samples.len(); while i < len { - let alpha = buffer.samples.get_mut(i).unwrap(); + let alpha = samples.get_mut(i).unwrap(); if alpha != &0xff { *alpha = 0xff; } // going one pixel further, still pointing to the alpha channel - i += buffer.layout.width_stride; + i += stride; } } debug!("Image dimensions: {}x{}", width, height); - Ok(ImageOnHeap::new(buffer)) + let channels = 4; + Ok(Frame::from_bgra(samples, channels, width, height)) + // let color = ColorType::Rgba8; + // let mut buffer = FlatSamples { + // samples, + // layout: SampleLayout::row_major_packed(channels, width as u32, height as u32), + // color_hint: Some(color), + // }; + // Ok(ImageOnHeap::new(buffer)) } fn get_active_window(&self) -> Result { @@ -262,6 +263,7 @@ impl PlatformApi for X11Api { mod test { use super::*; use crate::utils::IMG_EXT; + use crate::Image; use image::flat::View; use image::{save_buffer, GenericImageView, Rgba}; @@ -270,11 +272,13 @@ mod test { let mut api = X11Api::new()?; let win = api.get_active_window()?; let image_raw = api.capture_window_screenshot(win)?; + let image_raw: &Image = image_raw.as_ref(); let image: View<_, Rgba> = image_raw.as_view().unwrap(); let (width, height) = image.dimensions(); api.calibrate(win)?; let image_calibrated_raw = api.capture_window_screenshot(win)?; + let image_calibrated_raw: &Image = image_calibrated_raw.as_ref(); let image_calibrated: View<_, Rgba> = image_calibrated_raw.as_view().unwrap(); let (width_new, height_new) = image_calibrated.dimensions(); dbg!(width, width_new, height, height_new); @@ -336,6 +340,7 @@ mod test { let api = X11Api::new()?; let win = api.get_active_window()?; let image_raw = api.capture_window_screenshot(win)?; + let image_raw: &Image = image_raw.as_ref(); let image: View<_, Rgba> = image_raw.as_view().unwrap(); let (width, height) = image.dimensions(); diff --git a/src/macos/mod.rs b/src/macos/mod.rs index b9d0710..44ce2b4 100644 --- a/src/macos/mod.rs +++ b/src/macos/mod.rs @@ -3,10 +3,11 @@ mod screenshot; mod window_id; use crate::common::identify_transparency::identify_transparency; -use crate::common::image::crop; +use crate::common::image::CropMut; use crate::PlatformApi; -use crate::{ImageOnHeap, Margin, Result, WindowList}; +use crate::{Margin, Result, WindowList}; +use crate::common::Frame; use anyhow::Context; use screenshot::capture_window_screenshot; use std::env; @@ -25,7 +26,7 @@ struct QuartzApi { impl PlatformApi for QuartzApi { fn calibrate(&mut self, window_id: u64) -> Result<()> { let image = capture_window_screenshot(window_id)?; - self.margin = identify_transparency(*image)?; + self.margin = identify_transparency(image)?; Ok(()) } @@ -34,15 +35,16 @@ impl PlatformApi for QuartzApi { window_list() } - fn capture_window_screenshot(&self, window_id: u64) -> Result { - let img = capture_window_screenshot(window_id)?; + fn capture_window_screenshot(&self, window_id: u64) -> Result { + let mut frame = capture_window_screenshot(window_id)?; if let Some(margin) = self.margin.as_ref() { if !margin.is_zero() { // in this case we want to crop away the transparent margins - return crop(*img, margin); + frame.crop(margin)?; } } - Ok(img) + + Ok(frame) } fn get_active_window(&self) -> Result { diff --git a/src/macos/screenshot.rs b/src/macos/screenshot.rs index cfaf42a..228eddc 100644 --- a/src/macos/screenshot.rs +++ b/src/macos/screenshot.rs @@ -1,14 +1,11 @@ -use crate::common::image::convert_bgra_to_rgba; -use crate::ImageOnHeap; - use anyhow::{ensure, Context, Result}; use core_graphics::display::*; use core_graphics::image::CGImageRef; -use image::flat::SampleLayout; -use image::{ColorType, FlatSamples}; -pub fn capture_window_screenshot(win_id: u64) -> Result { - let (w, h, channels, mut raw_data) = { +use crate::common::Frame; + +pub fn capture_window_screenshot(win_id: u64) -> Result { + let (w, h, channels, raw_data) = { let image = unsafe { CGDisplay::screenshot( CGRectNull, @@ -44,26 +41,20 @@ pub fn capture_window_screenshot(win_id: u64) -> Result { (w, h, byte_per_pixel, raw_data) }; - convert_bgra_to_rgba(&mut raw_data); - - let color = ColorType::Rgba8; - let buffer = FlatSamples { - samples: raw_data, - layout: SampleLayout::row_major_packed(channels, w, h), - color_hint: Some(color), - }; - - Ok(ImageOnHeap::new(buffer)) + Ok(Frame::from_bgra(raw_data, channels, w, h)) } #[cfg(test)] mod tests { - use super::*; - #[cfg(feature = "e2e_tests")] - use crate::macos::setup; + use crate::Image; #[cfg(feature = "e2e_tests")] use image::save_buffer; + #[cfg(feature = "e2e_tests")] + use crate::macos::setup; + + use super::*; + #[test] #[should_panic(expected = "Cannot grab screenshot from CGDisplay of window id 999999")] fn should_throw_on_invalid_window_id() { @@ -79,6 +70,7 @@ mod tests { let mut api = setup()?; let win = 5308; let image = api.capture_window_screenshot(win)?; + let image: &Image = image.as_ref(); let (width, height) = (image.layout.width, image.layout.height); dbg!(width, height); @@ -94,6 +86,7 @@ mod tests { api.calibrate(win)?; let image_cropped = api.capture_window_screenshot(win)?; + let image_cropped: &Image = image_cropped.as_ref(); assert!(width > image_cropped.layout.width); // Note: visual validation is sometimes helpful: diff --git a/src/main.rs b/src/main.rs index 2027e24..09a068f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,7 +39,7 @@ use std::{env, thread}; use tempfile::TempDir; pub type Image = FlatSamples>; -pub type ImageOnHeap = Box; +pub type ImageOnHeap = Image; pub type WindowId = u64; pub type WindowList = Vec; pub type WindowListEntry = (Option, WindowId);