stream screen to display

This commit is contained in:
Vinzenz Schroeter 2025-02-12 20:30:27 +01:00
parent 9e299f69f6
commit ef19ab8b3f
8 changed files with 1775 additions and 15 deletions

1626
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -14,6 +14,8 @@ keywords = ["cccb", "cccb-servicepoint", "cli"]
[dependencies]
servicepoint = { version = "0.13.0", features = ["protocol_websocket"] }
clap = { version = "4.5", features = ["derive"]}
clap = { version = "4.5", features = ["derive"] }
env_logger = "0.11"
log = "0.4"
scap = "0.0.8"
image = "0.25.5"

View file

@ -1,5 +1,5 @@
{
description = "Flake for servicepoint-cli";
description = "Flake for command line interface of the ServicePoint display.";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
@ -55,12 +55,25 @@
};
nativeBuildInputs = with pkgs; [
pkg-config
libclang
rustPlatform.bindgenHook
];
strictDeps = true;
buildInputs = with pkgs; [
buildInputs =
with pkgs;
[
xe
xz
];
clang
]
++ lib.optionals pkgs.stdenv.isLinux (
with pkgs;
[
dbus
pipewire
libclang
]
);
};
default = servicepoint-cli;

View file

@ -1,3 +1,5 @@
use crate::stream_window::StreamScreenOptions;
#[derive(clap::Parser, std::fmt::Debug)]
#[clap(version, arg_required_else_help = true)]
pub struct Cli {
@ -38,8 +40,12 @@ pub enum Mode {
},
StreamStdin {
#[arg(long, short, default_value_t = false)]
slow: bool
}
slow: bool,
},
StreamScreen {
#[command(flatten)]
options: StreamScreenOptions,
},
}
#[derive(clap::Parser, std::fmt::Debug)]
@ -66,3 +72,4 @@ pub enum Protocol {
WebSocket,
Fake,
}

View file

@ -1,7 +1,8 @@
use crate::cli::{BrightnessCommand, Mode, PixelCommand};
use crate::stream_stdin::stream_stdin;
use crate::stream_window::stream_window;
use log::info;
use servicepoint::{Brightness, Command, Connection};
use crate::stream_stdin::stream_stdin;
pub fn execute_mode(mode: Mode, connection: Connection) {
match mode {
@ -11,7 +12,8 @@ pub fn execute_mode(mode: Mode, connection: Connection) {
}
Mode::Pixels { pixel_command } => pixels(&connection, pixel_command),
Mode::Brightness { brightness_command } => brightness(&connection, brightness_command),
Mode::StreamStdin{slow} => stream_stdin(&connection, slow),
Mode::StreamStdin { slow } => stream_stdin(&connection, slow),
Mode::StreamScreen { options } => stream_window(&connection, options),
}
}

View file

@ -6,6 +6,7 @@ use servicepoint::Connection;
mod cli;
mod execute;
mod stream_stdin;
mod stream_window;
fn main() {
let cli = Cli::parse();

View file

@ -1,6 +1,6 @@
use std::thread::sleep;
use log::warn;
use servicepoint::*;
use std::thread::sleep;
pub(crate) fn stream_stdin(connection: &Connection, slow: bool) {
warn!("This mode will break when using multi-byte characters and does not support ANSI escape sequences yet.");
@ -8,7 +8,7 @@ pub(crate) fn stream_stdin(connection: &Connection, slow: bool) {
connection,
mirror: CharGrid::new(TILE_WIDTH, TILE_HEIGHT),
y: 0,
slow
slow,
};
app.run()
}

115
src/stream_window.rs Normal file
View file

@ -0,0 +1,115 @@
use image::{
imageops::{dither, resize, BiLevel, FilterType},
DynamicImage, ImageBuffer, Rgb, Rgba,
};
use log::{debug, error, warn};
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::ops::Div;
use std::time::Duration;
#[derive(clap::Parser, std::fmt::Debug, Clone)]
pub struct StreamScreenOptions {
#[arg(long, short, default_value_t = true)]
pub dither: bool,
}
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,
);
if options.dither {
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 {
fps: FRAME_PACING
.div_duration_f32(Duration::from_secs(1))
.div(2f32) as u32,
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(),
)
}