use std::io::{self, Write};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use clap::Parser;
use windows_capture::capture::{Context, GraphicsCaptureApiHandler};
use windows_capture::encoder::{AudioSettingsBuilder, ContainerSettingsBuilder, VideoEncoder, VideoSettingsBuilder};
use windows_capture::frame::Frame;
use windows_capture::graphics_capture_api::InternalCaptureControl;
use windows_capture::monitor::Monitor;
use windows_capture::settings::{
ColorFormat, CursorCaptureSettings, DirtyRegionSettings, DrawBorderSettings, GraphicsCaptureItemType,
MinimumUpdateIntervalSettings, SecondaryWindowSettings, Settings,
};
use windows_capture::window::Window;
struct CaptureSettings {
stop_flag: Arc<AtomicBool>,
width: u32,
height: u32,
path: String,
bitrate: u32,
frame_rate: u32,
}
struct Capture {
encoder: Option<VideoEncoder>,
start: Instant,
frame_count_since_reset: u64,
last_reset: Instant,
settings: CaptureSettings,
}
impl GraphicsCaptureApiHandler for Capture {
type Flags = CaptureSettings;
type Error = Box<dyn std::error::Error + Send + Sync>;
fn new(ctx: Context<Self::Flags>) -> Result<Self, Self::Error> {
println!("Capture started. Press Ctrl+C to stop.");
let video_settings = VideoSettingsBuilder::new(ctx.flags.width, ctx.flags.height)
.bitrate(ctx.flags.bitrate)
.frame_rate(ctx.flags.frame_rate);
let encoder = VideoEncoder::new(
video_settings,
AudioSettingsBuilder::default().disabled(true),
ContainerSettingsBuilder::default(),
&ctx.flags.path,
)?;
Ok(Self {
encoder: Some(encoder),
start: Instant::now(),
frame_count_since_reset: 0,
last_reset: Instant::now(),
settings: ctx.flags,
})
}
fn on_frame_arrived(
&mut self,
frame: &mut Frame,
capture_control: InternalCaptureControl,
) -> Result<(), Self::Error> {
self.frame_count_since_reset += 1;
let elapsed_since_reset = self.last_reset.elapsed();
let fps = self.frame_count_since_reset as f64 / elapsed_since_reset.as_secs_f64();
print!("\rRecording for: {:.2}s | FPS: {:.2}", self.start.elapsed().as_secs_f64(), fps);
io::stdout().flush()?;
self.encoder.as_mut().unwrap().send_frame(frame)?;
if self.settings.stop_flag.load(Ordering::SeqCst) {
println!("\nStopping capture...");
self.encoder.take().unwrap().finish()?;
capture_control.stop();
println!("\nRecording stopped.");
}
if elapsed_since_reset >= Duration::from_secs(1) {
self.frame_count_since_reset = 0;
self.last_reset = Instant::now();
}
Ok(())
}
fn on_closed(&mut self) -> Result<(), Self::Error> {
println!("Capture item closed, stopping capture.");
self.settings.stop_flag.store(true, Ordering::SeqCst);
Ok(())
}
}
#[derive(Parser)]
#[command(name = "Screen Capture CLI")]
#[command(version = "1.0")]
#[command(author = "Your Name")]
#[command(about = "A simple command-line tool to capture a monitor or a window.")]
struct Cli {
#[arg(long, conflicts_with = "monitor_index")]
window_name: Option<String>,
#[arg(long, conflicts_with = "window_name")]
monitor_index: Option<u32>,
#[arg(long, default_value = "default")]
cursor_capture: String,
#[arg(long, default_value = "default")]
draw_border: String,
#[arg(long, default_value = "default")]
secondary_window: String,
#[arg(long)]
minimum_update_interval: Option<u64>,
#[arg(long, default_value = "default")]
dirty_region: String,
#[arg(long, default_value = "video.mp4")]
path: String,
#[arg(long, default_value_t = 15_000_000)]
bitrate: u32,
#[arg(long, default_value_t = 60)]
frame_rate: u32,
}
fn parse_cursor_capture(s: &str) -> CursorCaptureSettings {
match s.to_lowercase().as_str() {
"always" => CursorCaptureSettings::WithCursor,
"never" => CursorCaptureSettings::WithoutCursor,
"default" => CursorCaptureSettings::Default,
_ => {
eprintln!("Invalid cursor_capture value: '{}'. Use 'always', 'never', or 'default'.", s);
std::process::exit(1);
}
}
}
fn parse_draw_border(s: &str) -> DrawBorderSettings {
match s.to_lowercase().as_str() {
"always" => DrawBorderSettings::WithBorder,
"never" => DrawBorderSettings::WithoutBorder,
"default" => DrawBorderSettings::Default,
_ => {
eprintln!("Invalid draw_border value: '{}'. Use 'always', 'never', or 'default'.", s);
std::process::exit(1);
}
}
}
fn parse_secondary_window(s: &str) -> SecondaryWindowSettings {
match s.to_lowercase().as_str() {
"include" => SecondaryWindowSettings::Include,
"exclude" => SecondaryWindowSettings::Exclude,
"default" => SecondaryWindowSettings::Default,
_ => {
eprintln!("Invalid secondary_window value: '{}'. Use 'include', 'exclude', or 'default'.", s);
std::process::exit(1);
}
}
}
fn parse_minimum_update_interval(m: &Option<u64>) -> MinimumUpdateIntervalSettings {
match m {
Some(value) if *value > 0 => MinimumUpdateIntervalSettings::Custom(Duration::from_millis(*value)),
None | Some(0) => MinimumUpdateIntervalSettings::Default,
_ => {
eprintln!(
"Invalid minimum_update_interval value: '{}'. Use a positive integer or leave empty for default.",
m.unwrap_or(0)
);
std::process::exit(1);
}
}
}
fn parse_dirty_region(s: &str) -> DirtyRegionSettings {
match s.to_lowercase().as_str() {
"default" => DirtyRegionSettings::Default,
"report_only" => DirtyRegionSettings::ReportOnly,
"report_and_render" => DirtyRegionSettings::ReportAndRender,
_ => {
eprintln!("Invalid dirty_region value: '{}'. Use 'default', 'report_only', or 'report_and_render'.", s);
std::process::exit(1);
}
}
}
fn start_capture<T: TryInto<GraphicsCaptureItemType>>(
capture_item: T,
cursor_capture_settings: CursorCaptureSettings,
draw_border_settings: DrawBorderSettings,
secondary_window_settings: SecondaryWindowSettings,
minimum_update_interval_settings: MinimumUpdateIntervalSettings,
dirty_region_settings: DirtyRegionSettings,
settings: CaptureSettings,
) {
let capture_settings = Settings::new(
capture_item,
cursor_capture_settings,
draw_border_settings,
secondary_window_settings,
minimum_update_interval_settings,
dirty_region_settings,
ColorFormat::Bgra8,
settings,
);
Capture::start(capture_settings).expect("Screen capture failed");
}
fn main() {
let cli = Cli::parse();
let cursor_capture = parse_cursor_capture(&cli.cursor_capture);
let draw_border = parse_draw_border(&cli.draw_border);
let secondary_window = parse_secondary_window(&cli.secondary_window);
let minimum_update_interval = parse_minimum_update_interval(&cli.minimum_update_interval);
let dirty_region_settings = parse_dirty_region(&cli.dirty_region);
let stop_flag = Arc::new(AtomicBool::new(false));
{
let stop_flag = stop_flag.clone();
ctrlc::set_handler(move || {
stop_flag.store(true, Ordering::SeqCst);
})
.expect("Failed to set Ctrl+C handler");
}
if let Some(window_name) = cli.window_name {
let capture_item = Window::from_contains_name(&window_name)
.unwrap_or_else(|_| panic!("Window with name containing '{}' not found!", window_name));
let rect = capture_item.rect().expect("Failed to get window rect");
let width = (rect.right - rect.left) as u32;
let height = (rect.bottom - rect.top) as u32;
let capture_settings = CaptureSettings {
stop_flag: stop_flag.clone(),
width,
height,
path: cli.path.clone(),
bitrate: cli.bitrate,
frame_rate: cli.frame_rate,
};
println!("Capturing window: \"{}\"", capture_item.title().expect("Failed to get window title"));
println!("Window dimensions: {}x{}", width, height);
start_capture(
capture_item,
cursor_capture,
draw_border,
secondary_window,
minimum_update_interval,
dirty_region_settings,
capture_settings,
);
} else if let Some(index) = cli.monitor_index {
let capture_item = Monitor::from_index(usize::try_from(index).unwrap())
.unwrap_or_else(|_| panic!("Monitor with index {index} not found!"));
let width = capture_item.width().expect("Failed to get monitor width");
let height = capture_item.height().expect("Failed to get monitor height");
let capture_settings = CaptureSettings {
stop_flag: stop_flag.clone(),
width,
height,
path: cli.path.clone(),
bitrate: cli.bitrate,
frame_rate: cli.frame_rate,
};
println!("Capturing monitor {}", index);
println!("Monitor dimensions: {}x{}", width, height);
start_capture(
capture_item,
cursor_capture,
draw_border,
secondary_window,
minimum_update_interval,
dirty_region_settings,
capture_settings,
);
} else {
eprintln!(
"Error: You must specify either a window to capture with --window-name or a monitor with --monitor-index."
);
std::process::exit(1);
}
}