2025-02-12 20:30:27 +01:00
|
|
|
use image::{
|
|
|
|
imageops::{dither, resize, BiLevel, FilterType},
|
|
|
|
DynamicImage, ImageBuffer, Rgb, Rgba,
|
|
|
|
};
|
2025-02-12 20:46:34 +01:00
|
|
|
use log::{error, warn};
|
2025-02-12 20:30:27 +01:00
|
|
|
use scap::{
|
|
|
|
capturer::{Capturer, Options},
|
|
|
|
frame::convert_bgra_to_rgb,
|
|
|
|
frame::Frame,
|
|
|
|
};
|
|
|
|
use servicepoint::{
|
|
|
|
Bitmap, Command, CompressionCode, Connection, Origin, FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH,
|
|
|
|
};
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
|
|
#[derive(clap::Parser, std::fmt::Debug, Clone)]
|
|
|
|
pub struct StreamScreenOptions {
|
2025-02-12 20:46:34 +01:00
|
|
|
#[arg(long, short, default_value_t = false)]
|
|
|
|
pub no_dither: bool,
|
2025-02-12 20:30:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn stream_window(connection: &Connection, options: StreamScreenOptions) {
|
|
|
|
let capturer = match start_capture() {
|
|
|
|
Some(value) => value,
|
|
|
|
None => return,
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut bitmap = Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT);
|
|
|
|
loop {
|
|
|
|
let frame = capturer.get_next_frame().expect("failed to capture frame");
|
|
|
|
let frame = frame_to_image(frame);
|
|
|
|
let frame = frame.grayscale().to_luma8();
|
|
|
|
let mut frame = resize(
|
|
|
|
&frame,
|
|
|
|
PIXEL_WIDTH as u32,
|
|
|
|
PIXEL_HEIGHT as u32,
|
|
|
|
FilterType::Nearest,
|
|
|
|
);
|
|
|
|
|
2025-02-12 20:46:34 +01:00
|
|
|
if !options.no_dither {
|
2025-02-12 20:30:27 +01:00
|
|
|
dither(&mut frame, &BiLevel);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (mut dest, src) in bitmap.iter_mut().zip(frame.pixels()) {
|
|
|
|
*dest = src.0[0] > u8::MAX / 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
connection
|
|
|
|
.send(Command::BitmapLinearWin(
|
|
|
|
Origin::ZERO,
|
|
|
|
bitmap.clone(),
|
|
|
|
CompressionCode::Uncompressed,
|
|
|
|
))
|
|
|
|
.expect("failed to send frame to display");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn start_capture() -> Option<Capturer> {
|
|
|
|
if !scap::is_supported() {
|
|
|
|
error!("platform not supported by scap");
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
if !scap::has_permission() {
|
|
|
|
warn!("requesting screen recording permission");
|
|
|
|
if !scap::request_permission() {
|
|
|
|
error!("screen recording ermission denied");
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut capturer = Capturer::build(Options {
|
2025-02-12 20:46:34 +01:00
|
|
|
fps: FRAME_PACING.div_duration_f32(Duration::from_secs(1)) as u32,
|
2025-02-12 20:30:27 +01:00
|
|
|
target: None,
|
|
|
|
show_cursor: true,
|
|
|
|
show_highlight: true,
|
|
|
|
excluded_targets: None,
|
|
|
|
output_type: scap::frame::FrameType::BGR0,
|
|
|
|
..Default::default()
|
|
|
|
})
|
|
|
|
.expect("failed to create screen capture");
|
|
|
|
capturer.start_capture();
|
|
|
|
Some(capturer)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn frame_to_image(frame: Frame) -> DynamicImage {
|
|
|
|
match frame {
|
|
|
|
Frame::BGRx(frame) => bgrx_to_rgb(frame.width, frame.height, frame.data),
|
|
|
|
Frame::RGBx(frame) => DynamicImage::from(
|
|
|
|
ImageBuffer::<Rgba<_>, _>::from_raw(
|
|
|
|
frame.width as u32,
|
|
|
|
frame.height as u32,
|
|
|
|
frame.data,
|
|
|
|
)
|
|
|
|
.unwrap(),
|
|
|
|
),
|
|
|
|
Frame::BGR0(frame) => bgrx_to_rgb(frame.width, frame.height, frame.data),
|
|
|
|
Frame::RGB(frame) => DynamicImage::from(
|
|
|
|
ImageBuffer::<Rgb<_>, _>::from_raw(frame.width as u32, frame.height as u32, frame.data)
|
|
|
|
.unwrap(),
|
|
|
|
),
|
|
|
|
Frame::BGRA(frame) => bgrx_to_rgb(frame.width, frame.height, frame.data),
|
|
|
|
Frame::YUVFrame(_) | Frame::XBGR(_) => panic!("unsupported frame format"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn bgrx_to_rgb(width: i32, height: i32, data: Vec<u8>) -> DynamicImage {
|
|
|
|
DynamicImage::from(
|
|
|
|
ImageBuffer::<Rgb<_>, _>::from_raw(width as u32, height as u32, convert_bgra_to_rgb(data))
|
|
|
|
.unwrap(),
|
|
|
|
)
|
|
|
|
}
|