use std::sync::Arc;
use scap::capturer::{Capturer, Options};
use scap::frame::{Frame as ScapFrame, FrameType};
use scap::Target as ScapTarget;
use crate::error::CaptureError;
use crate::frame::Frame;
use crate::source::FrameSource;
use crate::target::{select_window, Target, WindowSelector};
const CAPTURE_FPS: u32 = 60;
pub struct ScapSource {
capturer: Capturer,
target: Target,
reattach: bool,
}
impl ScapSource {
pub fn new(target: &Target, reattach: bool) -> Result<Self, CaptureError> {
if !scap::is_supported() {
return Err(CaptureError::Backend("scap: platform not supported".into()));
}
if !scap::has_permission() && !scap::request_permission() {
return Err(CaptureError::PermissionDenied(
"grant Screen Recording in System Settings › Privacy & Security".into(),
));
}
let capturer = build_capturer(target)?;
Ok(Self {
capturer,
target: target.clone(),
reattach,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TargetInfo {
Display { index: usize, title: String },
Window { id: u32, title: String },
}
pub fn list_targets() -> Result<Vec<TargetInfo>, CaptureError> {
if !scap::has_permission() {
return Err(CaptureError::PermissionDenied(
"grant Screen Recording in System Settings › Privacy & Security".into(),
));
}
let mut out = Vec::new();
let mut display_index = 0;
for t in scap::get_all_targets() {
match t {
ScapTarget::Display(d) => {
out.push(TargetInfo::Display {
index: display_index,
title: d.title,
});
display_index += 1;
}
ScapTarget::Window(w) => out.push(TargetInfo::Window {
id: w.id,
title: w.title,
}),
}
}
Ok(out)
}
fn resolve_scap_target(target: &Target) -> Result<ScapTarget, CaptureError> {
let all = scap::get_all_targets();
let selector = match target {
Target::Display(index) => {
return all
.into_iter()
.filter(|t| matches!(t, ScapTarget::Display(_)))
.nth(*index)
.ok_or_else(|| CaptureError::TargetNotFound(format!("display index {index}")));
}
Target::WindowTitled(title) => WindowSelector::Exact(title),
Target::WindowContaining(sub) => WindowSelector::Containing(sub),
Target::WindowId(id) => WindowSelector::Id(*id),
};
let windows: Vec<(u32, String)> = all
.iter()
.filter_map(|t| match t {
ScapTarget::Window(w) => Some((w.id, w.title.clone())),
_ => None,
})
.collect();
let id = select_window(&windows, &selector)?;
all.into_iter()
.find(|t| matches!(t, ScapTarget::Window(w) if w.id == id))
.ok_or_else(|| CaptureError::TargetNotFound(format!("window id {id}")))
}
fn build_capturer(target: &Target) -> Result<Capturer, CaptureError> {
let scap_target = resolve_scap_target(target)?;
let options = Options {
fps: CAPTURE_FPS,
target: Some(scap_target),
output_type: FrameType::BGRAFrame,
show_cursor: true,
..Default::default()
};
std::panic::catch_unwind(std::panic::AssertUnwindSafe(
move || -> Result<Capturer, CaptureError> {
let mut capturer = Capturer::build(options)
.map_err(|e| CaptureError::Backend(format!("scap build: {e:?}")))?;
capturer
.start_capture()
.map_err(|e| CaptureError::Backend(format!("scap start: {e}")))?;
Ok(capturer)
},
))
.unwrap_or_else(|_| {
Err(CaptureError::Backend(
"capture backend panicked while starting".into(),
))
})
}
fn video_bgra_to_frame(width: i32, height: i32, data: Vec<u8>) -> Result<Arc<Frame>, CaptureError> {
if width <= 0 || height <= 0 {
return Err(CaptureError::Backend(format!(
"non-positive frame dimensions {width}x{height}"
)));
}
Frame::new(width as u32, height as u32, data).map(Arc::new)
}
#[async_trait::async_trait]
impl FrameSource for ScapSource {
async fn capture_frame(&mut self) -> Result<Arc<Frame>, CaptureError> {
match self.capturer.get_next_frame() {
Ok(ScapFrame::BGRA(f)) => video_bgra_to_frame(f.width, f.height, f.data),
Ok(_) => Err(CaptureError::Backend(
"scap returned a non-BGRA frame; expected FrameType::BGRAFrame".into(),
)),
Err(_) => {
if self.reattach {
if let Ok(capturer) = build_capturer(&self.target) {
self.capturer = capturer;
}
}
Err(CaptureError::TargetLost)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn converts_bgra_dimensions_and_rejects_bad_input() {
let good = video_bgra_to_frame(2, 2, vec![9u8; 16]).unwrap();
assert_eq!((good.width(), good.height()), (2, 2));
assert!(video_bgra_to_frame(-1, 2, vec![]).is_err());
assert!(video_bgra_to_frame(2, 2, vec![0u8; 15]).is_err());
}
}