Skip to content

Commit d745666

Browse files
committed
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
1 parent caf1b07 commit d745666

File tree

12 files changed

+147
-69
lines changed

12 files changed

+147
-69
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ env_logger = "0.9.0"
2727
simplerand = "1.3.0"
2828
humantime = "2.1.0"
2929
crossbeam-channel = "0.5"
30+
blockhash = "0.3"
3031

3132
[dependencies.clap]
3233
version = "3.1.9"

src/capture/frame.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use anyhow::Context;
2+
pub use blockhash::Image;
3+
use image::flat::View;
4+
use image::ColorType::Rgba8;
5+
use image::{save_buffer, FlatSamples, GenericImageView, Rgba};
6+
use tempfile::TempDir;
7+
8+
use crate::capture::Timecode;
9+
use crate::utils::IMG_EXT;
10+
use crate::{ImageOnHeap, Result};
11+
12+
pub struct Frame(FlatSamples<Vec<u8>>);
13+
14+
impl Frame {
15+
/// saves a frame as a IMG_EXT file
16+
pub fn save(
17+
&self,
18+
tc: &Timecode,
19+
tempdir: &TempDir,
20+
file_name_for: fn(&Timecode, &str) -> String,
21+
) -> Result<()> {
22+
let image = self.as_ref();
23+
save_buffer(
24+
tempdir.path().join(file_name_for(tc, IMG_EXT)),
25+
&image.samples,
26+
image.layout.width,
27+
image.layout.height,
28+
image.color_hint.unwrap_or(Rgba8),
29+
)
30+
.context("Cannot save frame")
31+
}
32+
}
33+
34+
impl Image for Frame {
35+
fn dimensions(&self) -> (u32, u32) {
36+
(self.0.layout.width, self.0.layout.height)
37+
}
38+
39+
fn get_pixel(&self, x: u32, y: u32) -> [u8; 4] {
40+
let image: View<_, Rgba<u8>> = self.0.as_view().unwrap();
41+
image.get_pixel(x, y).0
42+
}
43+
}
44+
45+
impl AsRef<FlatSamples<Vec<u8>>> for Frame {
46+
fn as_ref(&self) -> &FlatSamples<Vec<u8>> {
47+
&self.0
48+
}
49+
}
50+
51+
impl From<ImageOnHeap> for Frame {
52+
fn from(img: ImageOnHeap) -> Self {
53+
Self(*img)
54+
}
55+
}

src/capture/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
mod frame;
12
mod framerate;
23
mod processor;
4+
mod timecode;
35

6+
pub use frame::*;
47
pub use framerate::*;
58
pub use processor::*;
9+
pub use timecode::*;

src/capture/processor.rs

Lines changed: 47 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,40 @@
1-
use anyhow::Context;
1+
use blockhash::{blockhash256, Blockhash256};
22
use crossbeam_channel::Receiver;
3-
use image::save_buffer;
4-
use image::ColorType::Rgba8;
53
use rayon::ThreadPoolBuilder;
64
use std::borrow::Borrow;
75
use std::ops::Sub;
86
use std::sync::{Arc, Mutex};
97
use std::time::{Duration, Instant};
108
use tempfile::TempDir;
119

12-
use crate::capture::Framerate;
13-
use crate::utils::{file_name_for, IMG_EXT};
14-
use crate::{ImageOnHeap, PlatformApi, Result, WindowId};
10+
use crate::capture::{Framerate, Timecode};
11+
use crate::utils::file_name_for;
12+
use crate::{Frame, PlatformApi, Result, WindowId};
1513

16-
#[derive(Eq, PartialEq)]
14+
#[derive(Eq, PartialEq, Clone)]
1715
pub enum FrameDropStrategy {
1816
DoNotDropAny,
1917
DropIdenticalFrames,
2018
}
2119

2220
#[derive(Clone)]
23-
struct FrameComparator<'a> {
24-
last_frame: Option<ImageOnHeap>,
25-
strategy: &'a FrameDropStrategy,
21+
struct FrameComparator {
22+
last_frames: Vec<(Timecode, Timecode, Blockhash256)>,
23+
_strategy: FrameDropStrategy,
2624
}
2725

28-
impl<'a> FrameComparator<'a> {
29-
pub fn drop_frame(&mut self, frame: ImageOnHeap) -> bool {
30-
if self.last_frame.is_none() {
31-
self.last_frame = Some(frame);
32-
false
26+
impl FrameComparator {
27+
pub fn should_drop_frame(&mut self, timecode: &Timecode, frame: &Frame) -> bool {
28+
let hash = blockhash256(frame);
29+
if let Some((_last_time_code, _other_time_code, last_hash)) = self.last_frames.last() {
30+
let last_eq = last_hash == &hash;
31+
if !last_eq {
32+
self.last_frames.pop();
33+
self.last_frames.push((timecode.clone(), hash));
34+
}
35+
last_eq
3336
} else {
37+
self.last_frames.push((timecode.clone(), hash));
3438
false
3539
}
3640
}
@@ -43,23 +47,23 @@ pub fn capture_thread(
4347
rx: &Receiver<()>,
4448
api: impl PlatformApi + Sync,
4549
win_id: WindowId,
46-
time_codes: Arc<Mutex<Vec<u128>>>,
47-
tempdir: Arc<Mutex<TempDir>>,
50+
time_codes: Arc<Mutex<Vec<Timecode>>>,
51+
tempdir: Arc<TempDir>,
4852
frame_drop_strategy: &FrameDropStrategy,
4953
framerate: &Framerate,
5054
) -> Result<()> {
5155
let pool = ThreadPoolBuilder::default().build()?;
5256
let duration = Duration::from_secs(1) / *framerate.as_ref();
5357
let start = Instant::now();
54-
let mut idle_duration = Duration::from_millis(0);
55-
let mut last_frame: Option<ImageOnHeap> = None;
56-
let mut identical_frames = 0;
58+
// let mut idle_duration = Duration::from_millis(0);
59+
// let mut last_frame: Option<ImageOnHeap> = None;
60+
// let mut identical_frames = 0;
5761
let mut last_time = Instant::now();
5862
let api = Arc::new(api);
59-
let comp = Arc::new(FrameComparator {
60-
last_frame: None,
61-
strategy: frame_drop_strategy,
62-
});
63+
let comp = Arc::new(Mutex::new(FrameComparator {
64+
last_frames: Vec::new(),
65+
_strategy: frame_drop_strategy.clone(),
66+
}));
6367
// let rx = Arc::new(rx);
6468
// let mut results: Arc<Mutex<Vec<Result<()>>>> = Arc::new(Mutex::new(Vec::new()));
6569

@@ -72,26 +76,32 @@ pub fn capture_thread(
7276
if rx.recv_timeout(sleep_time).is_ok() {
7377
if pool.current_thread_has_pending_tasks().unwrap_or(false) {
7478
println!(
75-
"there is a backlog of frames that needs to be persisted, this may take a bit ...",
79+
"there is a backlog of frames that needs to be stored, this may take a bit ...",
7680
);
7781
}
7882
return;
7983
}
8084
let now = Instant::now();
81-
let timecode = now.saturating_duration_since(start).as_millis();
82-
// let effective_now = now.sub(idle_duration);
83-
let api = api.clone();
84-
let tempdir = tempdir.clone();
85-
time_codes.lock().unwrap().push(timecode);
86-
87-
s.spawn(move |_| {
88-
let frame = api.capture_window_screenshot(win_id);
85+
s.spawn({
86+
let api = api.clone();
87+
let tempdir = tempdir.clone();
88+
let comp = comp.clone();
89+
let time_codes = time_codes.clone();
90+
move |_| {
91+
let tc: Timecode = now.saturating_duration_since(start).as_millis().into();
8992

90-
if let Ok(frame) = frame {
91-
save_frame(&frame, timecode, tempdir.borrow(), file_name_for).unwrap();
92-
// results.borrow_mut().lock().unwrap().push(result);
93+
let frame = api.capture_window_screenshot(win_id);
94+
if let Ok(frame) = frame {
95+
let frame: Frame = frame.into();
96+
if comp.lock().unwrap().should_drop_frame(&tc, &frame) {
97+
return;
98+
}
99+
frame.save(&tc, tempdir.borrow(), file_name_for).unwrap();
100+
time_codes.lock().unwrap().push(tc);
101+
// results.borrow_mut().lock().unwrap().push(result);
102+
}
93103
}
94-
});
104+
});
95105

96106
/*
97107
let image = api.capture_window_screenshot(win_id)?;
@@ -129,20 +139,3 @@ pub fn capture_thread(
129139

130140
Ok(())
131141
}
132-
133-
/// saves a frame as a tga file
134-
pub fn save_frame(
135-
image: &ImageOnHeap,
136-
time_code: u128,
137-
tempdir: &TempDir,
138-
file_name_for: fn(&u128, &str) -> String,
139-
) -> Result<()> {
140-
save_buffer(
141-
tempdir.path().join(file_name_for(&time_code, IMG_EXT)),
142-
&image.samples,
143-
image.layout.width,
144-
image.layout.height,
145-
image.color_hint.unwrap_or(Rgba8),
146-
)
147-
.context("Cannot save frame")
148-
}

src/capture/timecode.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#[derive(Clone, Debug)]
2+
pub struct Timecode(u32);
3+
4+
impl AsRef<u32> for Timecode {
5+
fn as_ref(&self) -> &u32 {
6+
&self.0
7+
}
8+
}
9+
10+
impl From<u128> for Timecode {
11+
fn from(x: u128) -> Self {
12+
Self(x as u32)
13+
}
14+
}

src/common/image.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
use crate::common::Margin;
2-
use crate::{Image, ImageOnHeap, Result};
2+
use crate::{ImageOnHeap, Result};
33
use image::flat::View;
44
use image::{imageops, GenericImageView, ImageBuffer, Rgba};
55

66
///
77
/// specialized version of crop for [`ImageOnHeap`] and [`Margin`]
88
///
99
#[cfg_attr(not(macos), allow(dead_code))]
10-
pub fn crop(image: Image, margin: &Margin) -> Result<ImageOnHeap> {
10+
pub fn crop(image: crate::Image, margin: &Margin) -> Result<ImageOnHeap> {
1111
let mut img2: View<_, Rgba<u8>> = image.as_view()?;
1212
let (width, height) = (
13-
img2.width() - (margin.left + margin.right) as u32,
14-
img2.height() - (margin.top + margin.bottom) as u32,
13+
img2.dimensions().0 - (margin.left + margin.right) as u32,
14+
img2.dimensions().1 - (margin.top + margin.bottom) as u32,
1515
);
1616
let image_cropped = imageops::crop(
1717
&mut img2,

src/decor_effect.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::path::PathBuf;
22
use std::process::Command;
33

4+
use crate::capture::Timecode;
45
use anyhow::Context;
56
use rayon::prelude::*;
67
use tempfile::TempDir;
@@ -18,7 +19,7 @@ use crate::Result;
1819
/// -layers merge \
1920
/// t-rec-frame-000000251.tga
2021
/// ```
21-
pub fn apply_shadow_effect(time_codes: &[u128], tempdir: &TempDir, bg_color: String) {
22+
pub fn apply_shadow_effect(time_codes: &[Timecode], tempdir: &TempDir, bg_color: String) {
2223
apply_effect(
2324
time_codes,
2425
tempdir,
@@ -56,7 +57,7 @@ pub fn apply_shadow_effect(time_codes: &[u128], tempdir: &TempDir, bg_color: Str
5657
/// \) -alpha off -compose CopyOpacity -composite \
5758
/// t-rec-frame-000000251.tga
5859
/// ```
59-
pub fn apply_big_sur_corner_effect(time_codes: &[u128], tempdir: &TempDir) {
60+
pub fn apply_big_sur_corner_effect(time_codes: &[Timecode], tempdir: &TempDir) {
6061
let radius = 13;
6162
apply_effect(
6263
time_codes,
@@ -96,7 +97,7 @@ pub fn apply_big_sur_corner_effect(time_codes: &[u128], tempdir: &TempDir) {
9697
/// apply a given effect (closure) to all frames
9798
///
9899
fn apply_effect(
99-
time_codes: &[u128],
100+
time_codes: &[Timecode],
100101
tempdir: &TempDir,
101102
effect: Box<dyn Fn(PathBuf) -> Result<()> + Send + Sync>,
102103
) {

src/generators/gif.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::utils::{file_name_for, IMG_EXT};
22

3+
use crate::capture::Timecode;
34
use anyhow::{bail, Context, Result};
45
use std::ops::Div;
56
use std::process::{Command, Output};
@@ -27,7 +28,7 @@ pub fn check_for_imagemagick() -> Result<Output> {
2728
///
2829
/// generating the final gif with help of convert
2930
pub fn generate_gif_with_convert(
30-
time_codes: &[u128],
31+
time_codes: &[Timecode],
3132
tempdir: &TempDir,
3233
target: &str,
3334
start_pause: Option<Duration>,
@@ -43,10 +44,11 @@ pub fn generate_gif_with_convert(
4344
// houston we have a problem
4445
bail!("We got no frames :(");
4546
}
46-
let last_frame_i = *last_frame_i.unwrap();
47+
let last_frame_i = *last_frame_i.unwrap().as_ref();
4748
for (i, tc) in time_codes.iter().enumerate() {
49+
let tc = tc.as_ref();
4850
delay = *tc - delay;
49-
let frame = temp.join(file_name_for(tc, IMG_EXT));
51+
let frame = temp.join(file_name_for(&Timecode::from(*tc as u128), IMG_EXT));
5052
if !frame.exists() {
5153
continue;
5254
}
@@ -55,7 +57,7 @@ pub fn generate_gif_with_convert(
5557
(0, Some(delay), _) => {
5658
frame_delay += delay.as_millis().div(10) as u64;
5759
}
58-
(i, _, Some(delay)) if i as u128 == last_frame_i => {
60+
(i, _, Some(delay)) if i as u32 == last_frame_i => {
5961
frame_delay += delay.as_millis().div(10) as u64;
6062
}
6163
(_, _, _) => {}

src/generators/mp4.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::process::Command;
22

3+
use crate::capture::Timecode;
34
use anyhow::{Context, Result};
45
use tempfile::TempDir;
56

@@ -37,7 +38,7 @@ pub fn check_for_ffmpeg() -> Result<()> {
3738
///
3839
/// generating the final mp4 with help of ffmpeg
3940
pub fn generate_mp4_with_ffmpeg(
40-
_time_codes: &[u128],
41+
_time_codes: &[Timecode],
4142
tempdir: &TempDir,
4243
target: &str,
4344
) -> Result<()> {

0 commit comments

Comments
 (0)