use crate::cli::config::defaults::FPS;
use crate::cli::config::ProfileSettings;
use crate::cli::input::{HotkeyConfig, InputState, KeyboardMonitor};
use crate::cli::output::OutputConfig;
use crate::core::capture::{capture_thread, CaptureContext};
use crate::core::common::PlatformApi;
use crate::core::event_router::*;
use crate::core::screenshot::ScreenshotInfo;
use crate::core::types::{BackgroundColor, Decor};
use anyhow::Context;
use image::DynamicImage;
use std::io::{self, Write};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use tempfile::TempDir;
use tokio::sync::broadcast::Receiver;
use super::presenter::{create_presenter, Presenter};
use super::runtime::{Actor, Runtime};
use crate::core::{Result, WindowId};
#[cfg(unix)]
use crate::cli::pty::PtyShell;
#[cfg(target_os = "windows")]
use crate::cli::pty_windows::PtyShell;
#[derive(Clone)]
pub struct PostProcessConfig {
pub decor: Decor,
pub bg_color: BackgroundColor,
pub wallpaper: Option<(DynamicImage, u32)>,
pub start_delay: Duration,
pub end_delay: Duration,
pub fps: u8,
}
pub struct SessionConfig {
pub win_id: WindowId,
pub window_name: Option<String>,
pub program: String,
pub fps: u8,
pub natural: bool,
pub idle_pause: Option<Duration>,
pub output_path: PathBuf,
pub generate_gif: bool,
pub generate_video: bool,
pub verbose: bool,
pub quiet: bool,
pub post_process: PostProcessConfig,
}
impl SessionConfig {
pub fn builder() -> SessionConfigBuilder {
SessionConfigBuilder::default()
}
}
#[derive(Default)]
pub struct SessionConfigBuilder {
win_id: Option<WindowId>,
window_name: Option<String>,
program: Option<String>,
fps: Option<u8>,
natural: Option<bool>,
idle_pause: Option<Duration>,
output_path: Option<PathBuf>,
generate_gif: Option<bool>,
generate_video: Option<bool>,
verbose: Option<bool>,
quiet: Option<bool>,
decor: Option<Decor>,
bg_color: Option<BackgroundColor>,
wallpaper: Option<(DynamicImage, u32)>,
start_delay: Option<Duration>,
end_delay: Option<Duration>,
}
impl SessionConfigBuilder {
pub fn win_id(mut self, win_id: WindowId) -> Self {
self.win_id = Some(win_id);
self
}
pub fn window_name(mut self, name: Option<String>) -> Self {
self.window_name = name;
self
}
pub fn program(mut self, program: String) -> Self {
self.program = Some(program);
self
}
pub fn using_profile(mut self, settings: &ProfileSettings) -> Self {
self.fps = Some(settings.fps());
self.natural = Some(settings.natural());
self.verbose = Some(settings.verbose());
self.quiet = Some(settings.quiet());
self.generate_gif = Some(!settings.video_only());
self.generate_video = Some(settings.video() || settings.video_only());
self.output_path = Some(PathBuf::from(settings.output()));
self.decor = Some(settings.decor().parse().unwrap_or_default());
self.bg_color = Some(settings.bg().parse().unwrap_or_default());
self
}
pub fn idle_pause(mut self, duration: Option<Duration>) -> Self {
self.idle_pause = duration;
self
}
pub fn start_delay(mut self, delay: Duration) -> Self {
self.start_delay = Some(delay);
self
}
pub fn end_delay(mut self, delay: Duration) -> Self {
self.end_delay = Some(delay);
self
}
pub fn wallpaper(mut self, wallpaper: Option<(DynamicImage, u32)>) -> Self {
self.wallpaper = wallpaper;
self
}
pub fn build(self) -> SessionConfig {
SessionConfig {
win_id: self.win_id.expect("win_id is required"),
window_name: self.window_name,
program: self.program.expect("program is required"),
fps: self.fps.unwrap_or(FPS),
natural: self.natural.unwrap_or(false),
idle_pause: self.idle_pause,
output_path: self.output_path.unwrap_or_else(|| PathBuf::from("t-rec")),
generate_gif: self.generate_gif.unwrap_or(true),
generate_video: self.generate_video.unwrap_or(false),
verbose: self.verbose.unwrap_or(false),
quiet: self.quiet.unwrap_or(false),
post_process: PostProcessConfig {
decor: self.decor.unwrap_or_default(),
bg_color: self.bg_color.unwrap_or_default(),
wallpaper: self.wallpaper,
start_delay: self.start_delay.unwrap_or(Duration::ZERO),
end_delay: self.end_delay.unwrap_or(Duration::ZERO),
fps: self.fps.unwrap_or(FPS),
},
}
}
}
pub struct RecordingResult {
pub frame_count: usize,
pub screenshots: Vec<ScreenshotInfo>,
pub tempdir: Arc<Mutex<TempDir>>,
pub time_codes: Arc<Mutex<Vec<u128>>>,
}
pub struct RecordingSession {
config: SessionConfig,
api: Box<dyn PlatformApi>,
runtime: Runtime,
}
impl RecordingSession {
pub fn new(config: SessionConfig, api: Box<dyn PlatformApi>, runtime: Runtime) -> Result<Self> {
Ok(Self {
config,
api,
runtime,
})
}
pub fn run(self) -> Result<RecordingResult> {
let config = self.config;
let api = self.api;
let mut runtime = self.runtime;
let tempdir = Arc::new(Mutex::new(
TempDir::new().context("Cannot create tempdir.")?,
));
let time_codes = Arc::new(Mutex::new(Vec::new()));
let screenshots = Arc::new(Mutex::new(Vec::<ScreenshotInfo>::new()));
let router = EventRouter::new();
let input_state = Arc::new(InputState::new());
let idle_duration = Arc::new(Mutex::new(Duration::from_millis(0)));
let recording_start = Instant::now();
runtime.spawn(Actor::Photographer, {
let ctx = CaptureContext {
win_id: config.win_id,
time_codes: time_codes.clone(),
tempdir: tempdir.clone(),
natural: config.natural,
idle_pause: config.idle_pause,
fps: config.fps,
screenshots: Some(screenshots.clone()),
};
let event_rx = router.subscribe();
move || -> Result<()> { capture_thread(event_rx, api, ctx) }
});
clear_screen();
if config.verbose {
println!(
"[t-rec]: Frame cache dir: {:?}",
tempdir.lock().expect("Cannot lock tempdir resource").path()
);
if let Some(ref window) = config.window_name {
println!("[t-rec]: Recording window: {:?}", window);
} else {
println!("[t-rec]: Recording window id: {}", config.win_id);
}
}
let presenter = create_presenter(config.win_id);
let presenter_subscriber = router.subscribe();
if !config.quiet {
println!("[t-rec]: Press Ctrl+D to end recording");
println!("[t-rec]: F2 = Screenshot");
for i in (1..=3).rev() {
print!("\r[t-rec]: Recording starts in {}...", i);
io::stdout().flush().ok();
thread::sleep(Duration::from_secs(1));
}
print!("\r[t-rec]: Recording! \n");
io::stdout().flush().ok();
router.send(Event::Flash(FlashEvent::RecordingStarted));
thread::sleep(Duration::from_millis(250));
}
clear_screen();
Self::run_recording(
runtime,
&config,
router,
input_state,
idle_duration,
recording_start,
presenter_subscriber,
presenter,
)?;
print!("\x1b[2J\x1b[H");
io::stdout().flush().ok();
let frame_count = time_codes.lock().unwrap().len();
let screenshots_result = screenshots.lock().unwrap().clone();
Ok(RecordingResult {
frame_count,
screenshots: screenshots_result,
tempdir,
time_codes,
})
}
#[allow(clippy::too_many_arguments)]
fn run_recording(
mut runtime: Runtime,
config: &SessionConfig,
router: EventRouter,
input_state: Arc<InputState>,
idle_duration: Arc<Mutex<Duration>>,
recording_start: Instant,
presenter_subscriber: Receiver<Event>,
mut presenter: impl Presenter + 'static,
) -> Result<()> {
let mut pty_shell = PtyShell::spawn(&config.program)?;
let shell_stdin = pty_shell.get_writer()?;
runtime.spawn(Actor::ShellForwarder, {
let event_rx = router.subscribe();
move || pty_shell.forward_output(event_rx)
});
let hotkey_config = HotkeyConfig::default();
let keyboard_monitor = KeyboardMonitor::new(
input_state,
idle_duration,
recording_start,
hotkey_config,
router.clone(),
);
runtime.spawn(Actor::InputHandler, {
let event_rx = router.subscribe();
move || {
keyboard_monitor.run(shell_stdin, event_rx)?;
Ok(())
}
});
thread::sleep(Duration::from_millis(350));
router
.try_send(Event::Capture(CaptureEvent::Start))
.context("Cannot start capture thread")?;
presenter.run(presenter_subscriber)?;
router.shutdown();
runtime.join_all()?;
Ok(())
}
pub(crate) fn output_config(&self) -> crate::cli::output::OutputConfig {
OutputConfig {
output_path: self.config.output_path.clone(),
generate_gif: self.config.generate_gif,
generate_video: self.config.generate_video,
quiet: self.config.quiet,
post_process: self.config.post_process.clone(),
}
}
}
fn clear_screen() {
print!("\x1b[2J\x1b[H");
std::io::stdout().flush().ok();
}