Merge pull request 'Stream video files, update servicepoint, fix dithering' (#3) from next-sp-ver into main
All checks were successful
Rust / build (push) Successful in 9m12s

Reviewed-on: #3
This commit is contained in:
vinzenz 2025-05-04 17:48:34 +02:00
commit 0da50ed7dc
16 changed files with 677 additions and 286 deletions

View file

@ -26,10 +26,10 @@ jobs:
- name: Install rust toolchain - name: Install rust toolchain
run: sudo apt-get install -qy cargo-1.80 rust-1.80-clippy run: sudo apt-get install -qy cargo-1.80 rust-1.80-clippy
- name: Install system dependencies - name: Install system dependencies
run: sudo apt-get install -qy liblzma-dev libpipewire-0.3-dev libclang-dev libdbus-1-dev run: sudo apt-get install -qy liblzma-dev libpipewire-0.3-dev libclang-dev libdbus-1-dev ffmpeg libavutil-dev libavformat-dev libavfilter-dev libavdevice-dev
- name: Run Clippy - name: Run Clippy
run: cargo clippy --all-targets --all-features run: cargo clippy --all-targets --all-features
- name: Build - name: Build
run: cargo build --release --verbose run: cargo build --release

531
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
[package] [package]
name = "servicepoint-cli" name = "servicepoint-cli"
description = "A command line interface for the ServicePoint display." description = "A command line interface for the ServicePoint display."
version = "0.3.0" version = "0.4.0"
edition = "2021" edition = "2021"
rust-version = "1.80.0" rust-version = "1.80.0"
publish = true publish = true
@ -13,10 +13,20 @@ homepage = "https://crates.io/crates/servicepoint-cli"
keywords = ["cccb", "cccb-servicepoint", "cli"] keywords = ["cccb", "cccb-servicepoint", "cli"]
[dependencies] [dependencies]
servicepoint = { version = "0.13.2", features = ["protocol_websocket"] }
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }
env_logger = "0.11" env_logger = "0.11"
log = "0.4" log = "0.4"
scap = "0.0.8" scap = "0.0.8"
image = "0.25.5" image = "0.25.5"
fast_image_resize = { version = "5.1.2", features = ["image"] } fast_image_resize = { version = "5.1", features = ["image"] }
tungstenite = "0.26"
ffmpeg-next = "7.1.0"
[dependencies.servicepoint]
package = "servicepoint"
version = "0.14.1"
[profile.release]
lto = true # Enable link-time optimization
codegen-units = 1 # Reduce number of codegen units to increase optimizations
strip = true # Strip symbols from binary

View file

@ -1,5 +1,11 @@
# servicepoint-cli # servicepoint-cli
[![Release](https://git.berlin.ccc.de/servicepoint/servicepoint-cli/badges/release.svg)](https://git.berlin.ccc.de/servicepoint/servicepoint-cli/releases)
[![crates.io](https://img.shields.io/crates/v/servicepoint-cli.svg)](https://crates.io/crates/servicepoint-cli)
[![Crates.io Total Downloads](https://img.shields.io/crates/d/servicepoint-cli)](https://crates.io/crates/servicepoint-cli)
![GPLv3 licensed](https://img.shields.io/crates/l/servicepoint-cli)
[![CI](https://git.berlin.ccc.de/servicepoint/servicepoint-cli/badges/workflows/rust.yml/badge.svg)](https://git.berlin.ccc.de/servicepoint/servicepoint-cli)
This repository contains a command line interface for the ServicePoint display. This repository contains a command line interface for the ServicePoint display.
To send commands, this uses the [servicepoint crate](https://crates.io/crates/servicepoint). To send commands, this uses the [servicepoint crate](https://crates.io/crates/servicepoint).
@ -33,15 +39,15 @@ cargo run -- <args>
## Usage ## Usage
``` ```text
Usage: servicepoint-cli [OPTIONS] <COMMAND> Usage: servicepoint-cli [OPTIONS] <COMMAND>
Commands: Commands:
reset-everything Reset both pixels and brightness [aliases: r] reset Reset both pixels and brightness [aliases: r]
pixels Commands for manipulating pixels [aliases: p] pixels Commands for manipulating pixels [aliases: p]
brightness Commands for manipulating the brightness [aliases: b] brightness Commands for manipulating the brightness [aliases: b]
text Commands for sending text to the screen [aliases: t] text Commands for sending text to the screen [aliases: t]
help Print this message or the help of the given subcommand(s) help Print this message or the help of the given subcommand(s)
Options: Options:
-d, --destination <DESTINATION> ip:port of the servicepoint display [default: 127.0.0.1:2342] -d, --destination <DESTINATION> ip:port of the servicepoint display [default: 127.0.0.1:2342]
@ -53,7 +59,7 @@ Options:
### Pixels ### Pixels
``` ```text
Commands for manipulating pixels Commands for manipulating pixels
Usage: servicepoint-cli pixels <COMMAND> Usage: servicepoint-cli pixels <COMMAND>
@ -63,12 +69,13 @@ Commands:
flip Invert the state of all pixels [aliases: f] flip Invert the state of all pixels [aliases: f]
on Set all pixels to the on state on Set all pixels to the on state
image Send an image file (e.g. jpeg or png) to the display. [aliases: i] image Send an image file (e.g. jpeg or png) to the display. [aliases: i]
video Stream a video file (e.g. mp4) to the display. [aliases: v]
screen Stream the default screen capture source to the display. On Linux Wayland, this pops up a screen or window chooser, but it also may directly start streaming your main screen. [aliases: s] screen Stream the default screen capture source to the display. On Linux Wayland, this pops up a screen or window chooser, but it also may directly start streaming your main screen. [aliases: s]
``` ```
#### Image #### Image
``` ```text
Send an image file (e.g. jpeg or png) to the display. Send an image file (e.g. jpeg or png) to the display.
Usage: servicepoint-cli pixels image [OPTIONS] <FILE_NAME> Usage: servicepoint-cli pixels image [OPTIONS] <FILE_NAME>
@ -85,9 +92,28 @@ Options:
--no-aspect Do not keep aspect ratio when resizing. --no-aspect Do not keep aspect ratio when resizing.
``` ```
#### Video file
```text
Stream a video file (e.g. mp4) to the display.
Usage: servicepoint-cli pixels video [OPTIONS] <FILE_NAME>
Arguments:
<FILE_NAME>
Options:
--no-hist Disable histogram correction
--no-blur Disable blur
--no-sharp Disable sharpening
--no-dither Disable dithering. Brightness will be adjusted so that around half of the pixels are on.
--no-spacers Do not remove the spacers from the image.
--no-aspect Do not keep aspect ratio when resizing.
```
#### Screen #### Screen
``` ```text
Stream the default screen capture source to the display. On Linux Wayland, this pops up a screen or window chooser, but it also may directly start streaming your main screen. Stream the default screen capture source to the display. On Linux Wayland, this pops up a screen or window chooser, but it also may directly start streaming your main screen.
Usage: servicepoint-cli pixels screen [OPTIONS] Usage: servicepoint-cli pixels screen [OPTIONS]
@ -104,7 +130,7 @@ Options:
### Brightness ### Brightness
``` ```text
Commands for manipulating the brightness Commands for manipulating the brightness
Usage: servicepoint-cli brightness <COMMAND> Usage: servicepoint-cli brightness <COMMAND>
@ -117,18 +143,18 @@ Commands:
### Text ### Text
``` ```text
Commands for sending text to the screen Commands for sending text to the screen
Usage: servicepoint-cli text <COMMAND> Usage: servicepoint-cli text <COMMAND>
Commands: Commands:
stdin Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin` stdin Pipe text to the display, example: `journalctl | servicepoint-cli text stdin`
``` ```
#### Stdin #### Stdin
``` ```text
Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin` Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin`
Usage: servicepoint-cli stream stdin [OPTIONS] Usage: servicepoint-cli stream stdin [OPTIONS]
@ -137,6 +163,16 @@ Options:
-s, --slow Wait for a short amount of time before sending the next line -s, --slow Wait for a short amount of time before sending the next line
``` ```
### Reset
```text
Reset both pixels and brightness
Usage: servicepoint-cli reset [OPTIONS]
Options:
-f, --force hard reset screen
```
## Contributing ## Contributing

View file

@ -7,11 +7,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1739824009, "lastModified": 1745925850,
"narHash": "sha256-fcNrCMUWVLMG3gKC5M9CBqVOAnJtyRvGPxptQFl5mVg=", "narHash": "sha256-cyAAMal0aPrlb1NgzMxZqeN1mAJ2pJseDhm2m6Um8T0=",
"owner": "nix-community", "owner": "nix-community",
"repo": "naersk", "repo": "naersk",
"rev": "e5130d37369bfa600144c2424270c96f0ef0e11d", "rev": "38bc60bbc157ae266d4a0c96671c6c742ee17a5f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -37,11 +37,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1740603184, "lastModified": 1746183838,
"narHash": "sha256-t+VaahjQAWyA+Ctn2idyo1yxRIYpaDxMgHkgCNiMJa4=", "narHash": "sha256-kwaaguGkAqTZ1oK0yXeQ3ayYjs8u/W7eEfrFpFfIDFA=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "f44bd8ca21e026135061a0a57dcf3d0775b67a49", "rev": "bf3287dac860542719fe7554e21e686108716879",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -90,8 +90,7 @@
{ {
default = pkgs.mkShell rec { default = pkgs.mkShell rec {
inputsFrom = [ self.packages.${system}.default ]; inputsFrom = [ self.packages.${system}.default ];
packages = [ packages = with pkgs; [
pkgs.gdb
(pkgs.symlinkJoin { (pkgs.symlinkJoin {
name = "rust-toolchain"; name = "rust-toolchain";
paths = with pkgs; [ paths = with pkgs; [
@ -103,7 +102,11 @@
cargo-expand cargo-expand
]; ];
}) })
pkgs.cargo-flamegraph
cargo-flamegraph
gdb
ffmpeg-headless
]; ];
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath (builtins.concatMap (d: d.buildInputs) inputsFrom)}"; LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath (builtins.concatMap (d: d.buildInputs) inputsFrom)}";
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";

View file

@ -1,8 +1,8 @@
use crate::cli::BrightnessCommand; use crate::{cli::BrightnessCommand, transport::Transport};
use log::info; use log::info;
use servicepoint::{Brightness, Command, Connection}; use servicepoint::{Brightness, GlobalBrightnessCommand};
pub(crate) fn brightness(connection: &Connection, brightness_command: BrightnessCommand) { pub(crate) fn brightness(connection: &Transport, brightness_command: BrightnessCommand) {
match brightness_command { match brightness_command {
BrightnessCommand::Max => brightness_set(connection, Brightness::MAX), BrightnessCommand::Max => brightness_set(connection, Brightness::MAX),
BrightnessCommand::Min => brightness_set(connection, Brightness::MIN), BrightnessCommand::Min => brightness_set(connection, Brightness::MIN),
@ -12,9 +12,9 @@ pub(crate) fn brightness(connection: &Connection, brightness_command: Brightness
} }
} }
pub(crate) fn brightness_set(connection: &Connection, brightness: Brightness) { pub(crate) fn brightness_set(connection: &Transport, brightness: Brightness) {
connection connection
.send(Command::Brightness(brightness)) .send_command(GlobalBrightnessCommand::from(brightness))
.expect("Failed to set brightness"); .expect("Failed to set brightness");
info!("set brightness to {brightness:?}"); info!("set brightness to {brightness:?}");
} }

View file

@ -19,7 +19,7 @@ pub struct Cli {
value_enum, value_enum,
default_value = "udp" default_value = "udp"
)] )]
pub transport: Protocol, pub transport: TransportType,
#[clap(subcommand)] #[clap(subcommand)]
pub command: Mode, pub command: Mode,
#[clap(short, long, help = "verbose logging")] #[clap(short, long, help = "verbose logging")]
@ -29,7 +29,10 @@ pub struct Cli {
#[derive(clap::Parser, std::fmt::Debug)] #[derive(clap::Parser, std::fmt::Debug)]
pub enum Mode { pub enum Mode {
#[command(visible_alias = "r", about = "Reset both pixels and brightness")] #[command(visible_alias = "r", about = "Reset both pixels and brightness")]
ResetEverything, Reset {
#[arg(short, long, help = "hard reset screen")]
force: bool,
},
#[command(visible_alias = "p")] #[command(visible_alias = "p")]
Pixels { Pixels {
#[clap(subcommand)] #[clap(subcommand)]
@ -71,6 +74,16 @@ pub enum PixelCommand {
#[command(flatten)] #[command(flatten)]
image_processing_options: ImageProcessingOptions, image_processing_options: ImageProcessingOptions,
}, },
#[command(
visible_alias = "v",
about = "Stream a video file (e.g. mp4) to the display."
)]
Video {
#[command(flatten)]
send_image_options: SendImageOptions,
#[command(flatten)]
image_processing_options: ImageProcessingOptions,
},
#[command( #[command(
visible_alias = "s", visible_alias = "s",
about = "Stream the default screen capture source to the display. \ about = "Stream the default screen capture source to the display. \
@ -104,7 +117,7 @@ pub enum BrightnessCommand {
} }
#[derive(clap::ValueEnum, Clone, Debug)] #[derive(clap::ValueEnum, Clone, Debug)]
pub enum Protocol { pub enum TransportType {
Udp, Udp,
WebSocket, WebSocket,
Fake, Fake,
@ -114,7 +127,7 @@ pub enum Protocol {
#[clap(about = "Commands for sending text to the screen")] #[clap(about = "Commands for sending text to the screen")]
pub enum TextCommand { pub enum TextCommand {
#[command( #[command(
about = "Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin`" about = "Pipe text to the display, example: `journalctl | servicepoint-cli text stdin`"
)] )]
Stdin { Stdin {
#[arg( #[arg(

View file

@ -35,6 +35,7 @@ impl ImageProcessingPipeline {
} }
} }
#[must_use]
pub fn process(&mut self, frame: DynamicImage) -> Bitmap { pub fn process(&mut self, frame: DynamicImage) -> Bitmap {
let start_time = Instant::now(); let start_time = Instant::now();
@ -100,7 +101,7 @@ impl ImageProcessingPipeline {
let result = if self.options.no_dither { let result = if self.options.no_dither {
let cutoff = median_brightness(&orig); let cutoff = median_brightness(&orig);
let bits = orig.iter().map(move |x| x > &cutoff).collect(); let bits = orig.iter().map(move |x| x > &cutoff).collect();
Bitmap::from_bitvec(orig.width() as usize, bits) Bitmap::from_bitvec(orig.width() as usize, bits).unwrap()
} else { } else {
ostromoukhov_dither(orig, u8::MAX / 2) ostromoukhov_dither(orig, u8::MAX / 2)
}; };
@ -113,7 +114,7 @@ impl ImageProcessingPipeline {
let width = source.width(); let width = source.width();
let result_height = Self::calc_height_without_spacers(source.height()); let result_height = Self::calc_height_without_spacers(source.height());
let mut result = Bitmap::new(width, result_height); let mut result = Bitmap::new(width, result_height).unwrap();
let mut source_y = 0; let mut source_y = 0;
for result_y in 0..result_height { for result_y in 0..result_height {

View file

@ -1,7 +1,8 @@
//! Based on https://github.com/WarkerAnhaltRanger/CCCB_Ledwand //! Based on https://github.com/WarkerAnhaltRanger/CCCB_Ledwand
use image::GrayImage; use image::GrayImage;
use servicepoint::{BitVec, Bitmap, PIXEL_HEIGHT}; use log::debug;
use servicepoint::{Bitmap, DisplayBitVec, PIXEL_HEIGHT};
type GrayHistogram = [usize; 256]; type GrayHistogram = [usize; 256];
@ -169,10 +170,11 @@ pub(crate) fn ostromoukhov_dither(source: GrayImage, bias: u8) -> Bitmap {
assert_eq!(width % 8, 0); assert_eq!(width % 8, 0);
let mut source = source.into_raw(); let mut source = source.into_raw();
let mut destination = BitVec::repeat(false, source.len()); let mut destination = DisplayBitVec::repeat(false, source.len());
for y in 0..height as usize { for y in 0..height as usize {
let start = y * width as usize; let start = y * width as usize;
let last_row = y == (height - 1) as usize;
if y % 2 == 0 { if y % 2 == 0 {
for x in start..start + width as usize { for x in start..start + width as usize {
ostromoukhov_dither_pixel( ostromoukhov_dither_pixel(
@ -180,7 +182,7 @@ pub(crate) fn ostromoukhov_dither(source: GrayImage, bias: u8) -> Bitmap {
&mut destination, &mut destination,
x, x,
width as usize, width as usize,
y == (height - 1) as usize, last_row,
1, 1,
bias, bias,
); );
@ -192,7 +194,7 @@ pub(crate) fn ostromoukhov_dither(source: GrayImage, bias: u8) -> Bitmap {
&mut destination, &mut destination,
x, x,
width as usize, width as usize,
y == (height - 1) as usize, last_row,
-1, -1,
bias, bias,
); );
@ -200,13 +202,13 @@ pub(crate) fn ostromoukhov_dither(source: GrayImage, bias: u8) -> Bitmap {
} }
} }
Bitmap::from_bitvec(width as usize, destination) Bitmap::from_bitvec(width as usize, destination).unwrap()
} }
#[inline] #[inline]
fn ostromoukhov_dither_pixel( fn ostromoukhov_dither_pixel(
source: &mut [u8], source: &mut [u8],
destination: &mut BitVec, destination: &mut DisplayBitVec,
position: usize, position: usize,
width: usize, width: usize,
last_row: bool, last_row: bool,
@ -217,8 +219,16 @@ fn ostromoukhov_dither_pixel(
destination.set(position, destination_value); destination.set(position, destination_value);
let mut diffuse = |to: usize, mat: i16| { let mut diffuse = |to: usize, mat: i16| {
let diffuse_value = source[to] as i16 + mat; match source.get(to) {
source[to] = diffuse_value.clamp(u8::MIN.into(), u8::MAX.into()) as u8; None => {
// last row has a out of bounds error on the last pixel
// TODO fix the iter bounds instead of ignoring here
}
Some(val) => {
let diffuse_value = *val as i16 + mat;
source[to] = diffuse_value.clamp(u8::MIN.into(), u8::MAX.into()) as u8;
}
};
}; };
let lookup = if destination_value { let lookup = if destination_value {
@ -229,11 +239,14 @@ fn ostromoukhov_dither_pixel(
diffuse((position as isize + direction) as usize, lookup[0]); diffuse((position as isize + direction) as usize, lookup[0]);
if !last_row { if !last_row {
debug!("begin");
diffuse( diffuse(
((position + width) as isize - direction) as usize, ((position + width) as isize - direction) as usize,
lookup[1], lookup[1],
); );
debug!("mit");
diffuse(((position + width) as isize) as usize, lookup[2]); diffuse(((position + width) as isize) as usize, lookup[2]);
debug!("end");
} }
} }

View file

@ -1,12 +1,13 @@
use crate::{ use crate::{
brightness::{brightness, brightness_set}, brightness::{brightness, brightness_set},
cli::{Cli, Mode, Protocol}, cli::{Cli, Mode},
pixels::{pixels, pixels_off}, pixels::{pixels, pixels_off},
text::text text::text,
transport::Transport,
}; };
use clap::Parser; use clap::Parser;
use log::debug; use log::debug;
use servicepoint::{Brightness, Connection}; use servicepoint::{Brightness, HardResetCommand};
mod brightness; mod brightness;
mod cli; mod cli;
@ -16,40 +17,32 @@ mod pixels;
mod stream_stdin; mod stream_stdin;
mod stream_window; mod stream_window;
mod text; mod text;
mod transport;
fn main() { fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
init_logging(cli.verbose); init_logging(cli.verbose);
debug!("running with arguments: {:?}", cli); debug!("running with arguments: {:?}", cli);
let connection = make_connection(cli.destination, cli.transport); let transport = Transport::connect(cli.transport, &cli.destination);
debug!("connection established: {:#?}", connection); debug!("connection established: {:#?}", transport);
execute_mode(cli.command, connection); execute_mode(cli.command, transport);
} }
pub fn execute_mode(mode: Mode, connection: Connection) { pub fn execute_mode(mode: Mode, connection: Transport) {
match mode { match mode {
Mode::ResetEverything => { Mode::Reset { force } => {
brightness_set(&connection, Brightness::MAX); if force {
pixels_off(&connection); connection.send_command(HardResetCommand).unwrap()
} else {
brightness_set(&connection, Brightness::MAX);
pixels_off(&connection);
}
} }
Mode::Pixels { pixel_command } => pixels(&connection, pixel_command), Mode::Pixels { pixel_command } => pixels(&connection, pixel_command),
Mode::Brightness { brightness_command } => brightness(&connection, brightness_command), Mode::Brightness { brightness_command } => brightness(&connection, brightness_command),
Mode::Text { text_command} => text(&connection, text_command), Mode::Text { text_command } => text(&connection, text_command),
}
}
fn make_connection(destination: String, transport: Protocol) -> Connection {
match transport {
Protocol::Udp => Connection::open(destination).expect("Failed to open UDP connection"),
Protocol::WebSocket => {
let url = destination.parse().expect(
"provided destination is not a valid url - make sure it starts with 'ws://'",
);
Connection::open_websocket(url).expect("Failed to open WebSocket connection")
}
Protocol::Fake => Connection::Fake,
} }
} }

View file

@ -1,12 +1,18 @@
use crate::{ use crate::{
image_processing::ImageProcessingPipeline,
cli::{ImageProcessingOptions, PixelCommand, SendImageOptions}, cli::{ImageProcessingOptions, PixelCommand, SendImageOptions},
stream_window::stream_window image_processing::ImageProcessingPipeline,
stream_window::stream_window,
transport::Transport,
}; };
use ffmpeg_next as ffmpeg;
use image::{DynamicImage, RgbImage};
use log::info; use log::info;
use servicepoint::{BitVec, Command, CompressionCode, Connection, Origin, PIXEL_COUNT}; use servicepoint::{
BinaryOperation, BitVecCommand, BitmapCommand, ClearCommand, CompressionCode, DisplayBitVec,
Origin, PIXEL_COUNT,
};
pub(crate) fn pixels(connection: &Connection, pixel_command: PixelCommand) { pub(crate) fn pixels(connection: &Transport, pixel_command: PixelCommand) {
match pixel_command { match pixel_command {
PixelCommand::Off => pixels_off(connection), PixelCommand::Off => pixels_off(connection),
PixelCommand::Flip => pixels_invert(connection), PixelCommand::Flip => pixels_invert(connection),
@ -19,34 +25,50 @@ pub(crate) fn pixels(connection: &Connection, pixel_command: PixelCommand) {
stream_options, stream_options,
image_processing, image_processing,
} => stream_window(connection, stream_options, image_processing), } => stream_window(connection, stream_options, image_processing),
PixelCommand::Video {
image_processing_options: processing_options,
send_image_options: image_options,
} => pixels_video(connection, image_options, processing_options),
} }
} }
fn pixels_on(connection: &Connection) { fn pixels_on(connection: &Transport) {
let mask = BitVec::repeat(true, PIXEL_COUNT); let mask = DisplayBitVec::repeat(true, PIXEL_COUNT);
let command = BitVecCommand {
offset: 0,
bitvec: mask,
compression: CompressionCode::Lzma,
operation: BinaryOperation::Overwrite,
};
connection connection
.send(Command::BitmapLinear(0, mask, CompressionCode::Lzma)) .send_command(command)
.expect("could not send command"); .expect("could not send command");
info!("turned on all pixels") info!("turned on all pixels")
} }
fn pixels_invert(connection: &Connection) { fn pixels_invert(connection: &Transport) {
let mask = BitVec::repeat(true, PIXEL_COUNT); let mask = DisplayBitVec::repeat(true, PIXEL_COUNT);
let command = BitVecCommand {
offset: 0,
bitvec: mask,
compression: CompressionCode::Lzma,
operation: BinaryOperation::Xor,
};
connection connection
.send(Command::BitmapLinearXor(0, mask, CompressionCode::Lzma)) .send_command(command)
.expect("could not send command"); .expect("could not send command");
info!("inverted all pixels"); info!("inverted all pixels");
} }
pub(crate) fn pixels_off(connection: &Connection) { pub(crate) fn pixels_off(connection: &Transport) {
connection connection
.send(Command::Clear) .send_command(ClearCommand)
.expect("failed to clear pixels"); .expect("failed to clear pixels");
info!("reset pixels"); info!("reset pixels");
} }
fn pixels_image( fn pixels_image(
connection: &Connection, connection: &Transport,
options: SendImageOptions, options: SendImageOptions,
processing_options: ImageProcessingOptions, processing_options: ImageProcessingOptions,
) { ) {
@ -54,11 +76,82 @@ fn pixels_image(
let mut pipeline = ImageProcessingPipeline::new(processing_options); let mut pipeline = ImageProcessingPipeline::new(processing_options);
let bitmap = pipeline.process(image); let bitmap = pipeline.process(image);
connection connection
.send(Command::BitmapLinearWin( .send_command(BitmapCommand {
Origin::ZERO, origin: Origin::ZERO,
bitmap, bitmap,
CompressionCode::default(), compression: CompressionCode::default(),
)) })
.expect("failed to send image command"); .expect("failed to send image command");
info!("sent image to display"); info!("sent image to display");
} }
fn pixels_video(
connection: &Transport,
options: SendImageOptions,
processing_options: ImageProcessingOptions,
) {
ffmpeg::init().unwrap();
let mut ictx = ffmpeg::format::input(&options.file_name).expect("failed to open video input file");
let input = ictx
.streams()
.best(ffmpeg::media::Type::Video)
.ok_or(ffmpeg::Error::StreamNotFound)
.expect("could not get video stream from input file");
let video_stream_index = input.index();
let context_decoder = ffmpeg::codec::context::Context::from_parameters(input.parameters())
.expect("could not extract video context from parameters");
let mut decoder = context_decoder.decoder().video()
.expect("failed to create decoder for video stream");
let src_width = decoder.width();
let src_height = decoder.height();
let mut scaler = ffmpeg::software::scaling::Context::get(
decoder.format(),
src_width,
src_height,
ffmpeg::format::Pixel::RGB24,
src_width,
src_height,
ffmpeg::software::scaling::Flags::BILINEAR,
).expect("failed to create scaling context");
let mut frame_index = 0;
let mut processing_pipeline = ImageProcessingPipeline::new(processing_options);
let mut receive_and_process_decoded_frames =
|decoder: &mut ffmpeg::decoder::Video| -> Result<(), ffmpeg::Error> {
let mut decoded = ffmpeg::util::frame::video::Video::empty();
let mut rgb_frame = ffmpeg::util::frame::video::Video::empty();
while decoder.receive_frame(&mut decoded).is_ok() {
scaler.run(&decoded, &mut rgb_frame)
.expect("failed to scale frame");
let image = RgbImage::from_raw(src_width, src_height, rgb_frame.data(0).to_owned())
.expect("could not read rgb data to image");
let image = DynamicImage::from(image);
let bitmap= processing_pipeline.process(image);
connection.send_command(BitmapCommand::from(bitmap))
.expect("failed to send image command");
frame_index += 1;
}
Ok(())
};
for (stream, packet) in ictx.packets() {
if stream.index() == video_stream_index {
decoder.send_packet(&packet)
.expect("failed to send video packet");
receive_and_process_decoded_frames(&mut decoder)
.expect("failed to process video packet");
}
}
decoder.send_eof().expect("failed to send eof");
receive_and_process_decoded_frames(&mut decoder)
.expect("failed to eof packet");
}

View file

@ -1,8 +1,9 @@
use crate::transport::Transport;
use log::warn; use log::warn;
use servicepoint::*; use servicepoint::*;
use std::thread::sleep; use std::thread::sleep;
pub(crate) fn stream_stdin(connection: &Connection, slow: bool) { pub(crate) fn stream_stdin(connection: &Transport, slow: bool) {
warn!("This mode will break when using multi-byte characters and does not support ANSI escape sequences yet."); warn!("This mode will break when using multi-byte characters and does not support ANSI escape sequences yet.");
let mut app = App { let mut app = App {
connection, connection,
@ -14,7 +15,7 @@ pub(crate) fn stream_stdin(connection: &Connection, slow: bool) {
} }
struct App<'t> { struct App<'t> {
connection: &'t Connection, connection: &'t Transport,
mirror: CharGrid, mirror: CharGrid,
y: usize, y: usize,
slow: bool, slow: bool,
@ -23,7 +24,7 @@ struct App<'t> {
impl App<'_> { impl App<'_> {
fn run(&mut self) { fn run(&mut self) {
self.connection self.connection
.send(Command::Clear) .send_command(ClearCommand)
.expect("couldn't clear screen"); .expect("couldn't clear screen");
let last_y = self.mirror.height() - 1; let last_y = self.mirror.height() - 1;
for line in std::io::stdin().lines() { for line in std::io::stdin().lines() {
@ -63,10 +64,10 @@ impl App<'_> {
fn send_mirror(&self) { fn send_mirror(&self) {
self.connection self.connection
.send(Command::Utf8Data( .send_command(CharGridCommand {
Origin::ZERO, origin: Origin::ZERO,
self.mirror.clone(), grid: self.mirror.clone(),
)) })
.expect("couldn't send screen to display"); .expect("couldn't send screen to display");
} }
@ -76,7 +77,10 @@ impl App<'_> {
Self::line_onto_grid(&mut line_grid, 0, line); Self::line_onto_grid(&mut line_grid, 0, line);
Self::line_onto_grid(&mut self.mirror, self.y, line); Self::line_onto_grid(&mut self.mirror, self.y, line);
self.connection self.connection
.send(Command::Utf8Data(Origin::new(0, self.y), line_grid)) .send_command(CharGridCommand {
origin: Origin::new(0, self.y),
grid: line_grid,
})
.expect("couldn't send single line to screen"); .expect("couldn't send single line to screen");
} }
} }

View file

@ -1,6 +1,7 @@
use crate::{ use crate::{
cli::{ImageProcessingOptions, StreamScreenOptions}, cli::{ImageProcessingOptions, StreamScreenOptions},
image_processing::ImageProcessingPipeline, image_processing::ImageProcessingPipeline,
transport::Transport,
}; };
use image::{DynamicImage, ImageBuffer, Rgb, Rgba}; use image::{DynamicImage, ImageBuffer, Rgb, Rgba};
use log::{debug, error, info, trace, warn}; use log::{debug, error, info, trace, warn};
@ -9,11 +10,11 @@ use scap::{
frame::convert_bgra_to_rgb, frame::convert_bgra_to_rgb,
frame::Frame, frame::Frame,
}; };
use servicepoint::{Command, CompressionCode, Connection, Origin, FRAME_PACING}; use servicepoint::{BitmapCommand, CompressionCode, Origin, FRAME_PACING};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
pub fn stream_window( pub fn stream_window(
connection: &Connection, connection: &Transport,
options: StreamScreenOptions, options: StreamScreenOptions,
processing_options: ImageProcessingOptions, processing_options: ImageProcessingOptions,
) { ) {
@ -36,11 +37,11 @@ pub fn stream_window(
trace!("bitmap ready to send in: {:?}", start.elapsed()); trace!("bitmap ready to send in: {:?}", start.elapsed());
connection connection
.send(Command::BitmapLinearWin( .send_command(BitmapCommand {
Origin::ZERO, origin: Origin::ZERO,
bitmap.clone(), bitmap: bitmap.clone(),
CompressionCode::default(), compression: CompressionCode::default(),
)) })
.expect("failed to send frame to display"); .expect("failed to send frame to display");
debug!("frame time: {:?}", start.elapsed()); debug!("frame time: {:?}", start.elapsed());

View file

@ -1,7 +1,7 @@
use servicepoint::Connection; use crate::{cli::TextCommand, stream_stdin::stream_stdin, transport::Transport};
use crate::cli::TextCommand;
use crate::stream_stdin::stream_stdin;
pub fn text(connection: &Connection, command: TextCommand) { pub fn text(connection: &Transport, command: TextCommand) {
match command { TextCommand::Stdin { slow } => stream_stdin(connection, slow), } match command {
TextCommand::Stdin { slow } => stream_stdin(connection, slow),
}
} }

51
src/transport.rs Normal file
View file

@ -0,0 +1,51 @@
use crate::cli::TransportType;
use servicepoint::{FakeConnection, Packet, UdpSocketExt};
use std::fmt::Debug;
use std::net::{TcpStream, UdpSocket};
use std::sync::Mutex;
use tungstenite::client::IntoClientRequest;
use tungstenite::stream::MaybeTlsStream;
use tungstenite::{ClientRequestBuilder, WebSocket};
#[derive(Debug)]
pub enum Transport {
Fake,
Udp(UdpSocket),
WebSocket(Mutex<WebSocket<MaybeTlsStream<TcpStream>>>),
}
impl Transport {
pub fn connect(kind: TransportType, destination: &str) -> Transport {
match kind {
TransportType::Udp => {
Self::Udp(UdpSocket::bind_connect(destination).expect("failed to bind socket"))
}
TransportType::WebSocket => {
let request = ClientRequestBuilder::new(
destination.parse().expect("Invalid destination url"),
)
.into_client_request()
.unwrap();
let (sock, _) =
tungstenite::connect(request).expect("failed to connect to websocket");
Self::WebSocket(Mutex::new(sock))
}
TransportType::Fake => Self::Fake,
}
}
pub(crate) fn send_command<T: TryInto<Packet>>(&self, command: T) -> Option<()>
where
<T as TryInto<Packet>>::Error: Debug,
{
match self {
Self::Udp(socket) => socket.send_command(command),
Self::WebSocket(socket) => {
let bytes: Vec<u8> = command.try_into().unwrap().into();
let mut socket = socket.lock().unwrap();
socket.send(tungstenite::Message::Binary(bytes.into())).ok()
}
Self::Fake => FakeConnection.send_command(command),
}
}
}