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
All checks were successful
Rust / build (push) Successful in 9m12s
Reviewed-on: #3
This commit is contained in:
commit
0da50ed7dc
4
.github/workflows/rust.yml
vendored
4
.github/workflows/rust.yml
vendored
|
@ -26,10 +26,10 @@ jobs:
|
|||
- name: Install rust toolchain
|
||||
run: sudo apt-get install -qy cargo-1.80 rust-1.80-clippy
|
||||
- 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
|
||||
run: cargo clippy --all-targets --all-features
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --verbose
|
||||
run: cargo build --release
|
||||
|
|
531
Cargo.lock
generated
531
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
16
Cargo.toml
16
Cargo.toml
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "servicepoint-cli"
|
||||
description = "A command line interface for the ServicePoint display."
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.80.0"
|
||||
publish = true
|
||||
|
@ -13,10 +13,20 @@ homepage = "https://crates.io/crates/servicepoint-cli"
|
|||
keywords = ["cccb", "cccb-servicepoint", "cli"]
|
||||
|
||||
[dependencies]
|
||||
servicepoint = { version = "0.13.2", features = ["protocol_websocket"] }
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
env_logger = "0.11"
|
||||
log = "0.4"
|
||||
scap = "0.0.8"
|
||||
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
|
||||
|
|
54
README.md
54
README.md
|
@ -1,5 +1,11 @@
|
|||
# servicepoint-cli
|
||||
|
||||
[](https://git.berlin.ccc.de/servicepoint/servicepoint-cli/releases)
|
||||
[](https://crates.io/crates/servicepoint-cli)
|
||||
[](https://crates.io/crates/servicepoint-cli)
|
||||

|
||||
[](https://git.berlin.ccc.de/servicepoint/servicepoint-cli)
|
||||
|
||||
This repository contains a command line interface for the ServicePoint display.
|
||||
|
||||
To send commands, this uses the [servicepoint crate](https://crates.io/crates/servicepoint).
|
||||
|
@ -33,11 +39,11 @@ cargo run -- <args>
|
|||
|
||||
## Usage
|
||||
|
||||
```
|
||||
```text
|
||||
Usage: servicepoint-cli [OPTIONS] <COMMAND>
|
||||
|
||||
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]
|
||||
brightness Commands for manipulating the brightness [aliases: b]
|
||||
text Commands for sending text to the screen [aliases: t]
|
||||
|
@ -53,7 +59,7 @@ Options:
|
|||
|
||||
### Pixels
|
||||
|
||||
```
|
||||
```text
|
||||
Commands for manipulating pixels
|
||||
|
||||
Usage: servicepoint-cli pixels <COMMAND>
|
||||
|
@ -63,12 +69,13 @@ Commands:
|
|||
flip Invert the state of all pixels [aliases: f]
|
||||
on Set all pixels to the on state
|
||||
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]
|
||||
```
|
||||
|
||||
#### Image
|
||||
|
||||
```
|
||||
```text
|
||||
Send an image file (e.g. jpeg or png) to the display.
|
||||
|
||||
Usage: servicepoint-cli pixels image [OPTIONS] <FILE_NAME>
|
||||
|
@ -85,9 +92,28 @@ Options:
|
|||
--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
|
||||
|
||||
```
|
||||
```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.
|
||||
|
||||
Usage: servicepoint-cli pixels screen [OPTIONS]
|
||||
|
@ -104,7 +130,7 @@ Options:
|
|||
|
||||
### Brightness
|
||||
|
||||
```
|
||||
```text
|
||||
Commands for manipulating the brightness
|
||||
|
||||
Usage: servicepoint-cli brightness <COMMAND>
|
||||
|
@ -117,18 +143,18 @@ Commands:
|
|||
|
||||
### Text
|
||||
|
||||
```
|
||||
```text
|
||||
Commands for sending text to the screen
|
||||
|
||||
Usage: servicepoint-cli text <COMMAND>
|
||||
|
||||
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
|
||||
|
||||
```
|
||||
```text
|
||||
Pipe text to the display, example: `journalctl | servicepoint-cli stream stdin`
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
### Reset
|
||||
|
||||
```text
|
||||
Reset both pixels and brightness
|
||||
|
||||
Usage: servicepoint-cli reset [OPTIONS]
|
||||
|
||||
Options:
|
||||
-f, --force hard reset screen
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
12
flake.lock
12
flake.lock
|
@ -7,11 +7,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1739824009,
|
||||
"narHash": "sha256-fcNrCMUWVLMG3gKC5M9CBqVOAnJtyRvGPxptQFl5mVg=",
|
||||
"lastModified": 1745925850,
|
||||
"narHash": "sha256-cyAAMal0aPrlb1NgzMxZqeN1mAJ2pJseDhm2m6Um8T0=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "e5130d37369bfa600144c2424270c96f0ef0e11d",
|
||||
"rev": "38bc60bbc157ae266d4a0c96671c6c742ee17a5f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -37,11 +37,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1740603184,
|
||||
"narHash": "sha256-t+VaahjQAWyA+Ctn2idyo1yxRIYpaDxMgHkgCNiMJa4=",
|
||||
"lastModified": 1746183838,
|
||||
"narHash": "sha256-kwaaguGkAqTZ1oK0yXeQ3ayYjs8u/W7eEfrFpFfIDFA=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f44bd8ca21e026135061a0a57dcf3d0775b67a49",
|
||||
"rev": "bf3287dac860542719fe7554e21e686108716879",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
@ -90,8 +90,7 @@
|
|||
{
|
||||
default = pkgs.mkShell rec {
|
||||
inputsFrom = [ self.packages.${system}.default ];
|
||||
packages = [
|
||||
pkgs.gdb
|
||||
packages = with pkgs; [
|
||||
(pkgs.symlinkJoin {
|
||||
name = "rust-toolchain";
|
||||
paths = with pkgs; [
|
||||
|
@ -103,7 +102,11 @@
|
|||
cargo-expand
|
||||
];
|
||||
})
|
||||
pkgs.cargo-flamegraph
|
||||
|
||||
cargo-flamegraph
|
||||
gdb
|
||||
|
||||
ffmpeg-headless
|
||||
];
|
||||
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath (builtins.concatMap (d: d.buildInputs) inputsFrom)}";
|
||||
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use crate::cli::BrightnessCommand;
|
||||
use crate::{cli::BrightnessCommand, transport::Transport};
|
||||
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 {
|
||||
BrightnessCommand::Max => brightness_set(connection, Brightness::MAX),
|
||||
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
|
||||
.send(Command::Brightness(brightness))
|
||||
.send_command(GlobalBrightnessCommand::from(brightness))
|
||||
.expect("Failed to set brightness");
|
||||
info!("set brightness to {brightness:?}");
|
||||
}
|
||||
|
|
21
src/cli.rs
21
src/cli.rs
|
@ -19,7 +19,7 @@ pub struct Cli {
|
|||
value_enum,
|
||||
default_value = "udp"
|
||||
)]
|
||||
pub transport: Protocol,
|
||||
pub transport: TransportType,
|
||||
#[clap(subcommand)]
|
||||
pub command: Mode,
|
||||
#[clap(short, long, help = "verbose logging")]
|
||||
|
@ -29,7 +29,10 @@ pub struct Cli {
|
|||
#[derive(clap::Parser, std::fmt::Debug)]
|
||||
pub enum Mode {
|
||||
#[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")]
|
||||
Pixels {
|
||||
#[clap(subcommand)]
|
||||
|
@ -71,6 +74,16 @@ pub enum PixelCommand {
|
|||
#[command(flatten)]
|
||||
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(
|
||||
visible_alias = "s",
|
||||
about = "Stream the default screen capture source to the display. \
|
||||
|
@ -104,7 +117,7 @@ pub enum BrightnessCommand {
|
|||
}
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Debug)]
|
||||
pub enum Protocol {
|
||||
pub enum TransportType {
|
||||
Udp,
|
||||
WebSocket,
|
||||
Fake,
|
||||
|
@ -114,7 +127,7 @@ pub enum Protocol {
|
|||
#[clap(about = "Commands for sending text to the screen")]
|
||||
pub enum TextCommand {
|
||||
#[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 {
|
||||
#[arg(
|
||||
|
|
|
@ -35,6 +35,7 @@ impl ImageProcessingPipeline {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn process(&mut self, frame: DynamicImage) -> Bitmap {
|
||||
let start_time = Instant::now();
|
||||
|
||||
|
@ -100,7 +101,7 @@ impl ImageProcessingPipeline {
|
|||
let result = if self.options.no_dither {
|
||||
let cutoff = median_brightness(&orig);
|
||||
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 {
|
||||
ostromoukhov_dither(orig, u8::MAX / 2)
|
||||
};
|
||||
|
@ -113,7 +114,7 @@ impl ImageProcessingPipeline {
|
|||
|
||||
let width = source.width();
|
||||
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;
|
||||
for result_y in 0..result_height {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
//! Based on https://github.com/WarkerAnhaltRanger/CCCB_Ledwand
|
||||
|
||||
use image::GrayImage;
|
||||
use servicepoint::{BitVec, Bitmap, PIXEL_HEIGHT};
|
||||
use log::debug;
|
||||
use servicepoint::{Bitmap, DisplayBitVec, PIXEL_HEIGHT};
|
||||
|
||||
type GrayHistogram = [usize; 256];
|
||||
|
||||
|
@ -169,10 +170,11 @@ pub(crate) fn ostromoukhov_dither(source: GrayImage, bias: u8) -> Bitmap {
|
|||
assert_eq!(width % 8, 0);
|
||||
|
||||
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 {
|
||||
let start = y * width as usize;
|
||||
let last_row = y == (height - 1) as usize;
|
||||
if y % 2 == 0 {
|
||||
for x in start..start + width as usize {
|
||||
ostromoukhov_dither_pixel(
|
||||
|
@ -180,7 +182,7 @@ pub(crate) fn ostromoukhov_dither(source: GrayImage, bias: u8) -> Bitmap {
|
|||
&mut destination,
|
||||
x,
|
||||
width as usize,
|
||||
y == (height - 1) as usize,
|
||||
last_row,
|
||||
1,
|
||||
bias,
|
||||
);
|
||||
|
@ -192,7 +194,7 @@ pub(crate) fn ostromoukhov_dither(source: GrayImage, bias: u8) -> Bitmap {
|
|||
&mut destination,
|
||||
x,
|
||||
width as usize,
|
||||
y == (height - 1) as usize,
|
||||
last_row,
|
||||
-1,
|
||||
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]
|
||||
fn ostromoukhov_dither_pixel(
|
||||
source: &mut [u8],
|
||||
destination: &mut BitVec,
|
||||
destination: &mut DisplayBitVec,
|
||||
position: usize,
|
||||
width: usize,
|
||||
last_row: bool,
|
||||
|
@ -217,8 +219,16 @@ fn ostromoukhov_dither_pixel(
|
|||
destination.set(position, destination_value);
|
||||
|
||||
let mut diffuse = |to: usize, mat: i16| {
|
||||
let diffuse_value = source[to] as i16 + mat;
|
||||
match source.get(to) {
|
||||
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 {
|
||||
|
@ -229,11 +239,14 @@ fn ostromoukhov_dither_pixel(
|
|||
diffuse((position as isize + direction) as usize, lookup[0]);
|
||||
|
||||
if !last_row {
|
||||
debug!("begin");
|
||||
diffuse(
|
||||
((position + width) as isize - direction) as usize,
|
||||
lookup[1],
|
||||
);
|
||||
debug!("mit");
|
||||
diffuse(((position + width) as isize) as usize, lookup[2]);
|
||||
debug!("end");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
35
src/main.rs
35
src/main.rs
|
@ -1,12 +1,13 @@
|
|||
use crate::{
|
||||
brightness::{brightness, brightness_set},
|
||||
cli::{Cli, Mode, Protocol},
|
||||
cli::{Cli, Mode},
|
||||
pixels::{pixels, pixels_off},
|
||||
text::text
|
||||
text::text,
|
||||
transport::Transport,
|
||||
};
|
||||
use clap::Parser;
|
||||
use log::debug;
|
||||
use servicepoint::{Brightness, Connection};
|
||||
use servicepoint::{Brightness, HardResetCommand};
|
||||
|
||||
mod brightness;
|
||||
mod cli;
|
||||
|
@ -16,43 +17,35 @@ mod pixels;
|
|||
mod stream_stdin;
|
||||
mod stream_window;
|
||||
mod text;
|
||||
mod transport;
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
init_logging(cli.verbose);
|
||||
debug!("running with arguments: {:?}", cli);
|
||||
|
||||
let connection = make_connection(cli.destination, cli.transport);
|
||||
debug!("connection established: {:#?}", connection);
|
||||
let transport = Transport::connect(cli.transport, &cli.destination);
|
||||
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 {
|
||||
Mode::ResetEverything => {
|
||||
Mode::Reset { force } => {
|
||||
if force {
|
||||
connection.send_command(HardResetCommand).unwrap()
|
||||
} else {
|
||||
brightness_set(&connection, Brightness::MAX);
|
||||
pixels_off(&connection);
|
||||
}
|
||||
}
|
||||
Mode::Pixels { pixel_command } => pixels(&connection, pixel_command),
|
||||
Mode::Brightness { brightness_command } => brightness(&connection, brightness_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,
|
||||
}
|
||||
}
|
||||
|
||||
fn init_logging(debug: bool) {
|
||||
let filter = if debug {
|
||||
log::LevelFilter::Debug
|
||||
|
|
127
src/pixels.rs
127
src/pixels.rs
|
@ -1,12 +1,18 @@
|
|||
use crate::{
|
||||
image_processing::ImageProcessingPipeline,
|
||||
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 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 {
|
||||
PixelCommand::Off => pixels_off(connection),
|
||||
PixelCommand::Flip => pixels_invert(connection),
|
||||
|
@ -19,34 +25,50 @@ pub(crate) fn pixels(connection: &Connection, pixel_command: PixelCommand) {
|
|||
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) {
|
||||
let mask = BitVec::repeat(true, PIXEL_COUNT);
|
||||
fn pixels_on(connection: &Transport) {
|
||||
let mask = DisplayBitVec::repeat(true, PIXEL_COUNT);
|
||||
let command = BitVecCommand {
|
||||
offset: 0,
|
||||
bitvec: mask,
|
||||
compression: CompressionCode::Lzma,
|
||||
operation: BinaryOperation::Overwrite,
|
||||
};
|
||||
connection
|
||||
.send(Command::BitmapLinear(0, mask, CompressionCode::Lzma))
|
||||
.send_command(command)
|
||||
.expect("could not send command");
|
||||
info!("turned on all pixels")
|
||||
}
|
||||
|
||||
fn pixels_invert(connection: &Connection) {
|
||||
let mask = BitVec::repeat(true, PIXEL_COUNT);
|
||||
fn pixels_invert(connection: &Transport) {
|
||||
let mask = DisplayBitVec::repeat(true, PIXEL_COUNT);
|
||||
let command = BitVecCommand {
|
||||
offset: 0,
|
||||
bitvec: mask,
|
||||
compression: CompressionCode::Lzma,
|
||||
operation: BinaryOperation::Xor,
|
||||
};
|
||||
connection
|
||||
.send(Command::BitmapLinearXor(0, mask, CompressionCode::Lzma))
|
||||
.send_command(command)
|
||||
.expect("could not send command");
|
||||
info!("inverted all pixels");
|
||||
}
|
||||
|
||||
pub(crate) fn pixels_off(connection: &Connection) {
|
||||
pub(crate) fn pixels_off(connection: &Transport) {
|
||||
connection
|
||||
.send(Command::Clear)
|
||||
.send_command(ClearCommand)
|
||||
.expect("failed to clear pixels");
|
||||
info!("reset pixels");
|
||||
}
|
||||
|
||||
fn pixels_image(
|
||||
connection: &Connection,
|
||||
connection: &Transport,
|
||||
options: SendImageOptions,
|
||||
processing_options: ImageProcessingOptions,
|
||||
) {
|
||||
|
@ -54,11 +76,82 @@ fn pixels_image(
|
|||
let mut pipeline = ImageProcessingPipeline::new(processing_options);
|
||||
let bitmap = pipeline.process(image);
|
||||
connection
|
||||
.send(Command::BitmapLinearWin(
|
||||
Origin::ZERO,
|
||||
.send_command(BitmapCommand {
|
||||
origin: Origin::ZERO,
|
||||
bitmap,
|
||||
CompressionCode::default(),
|
||||
))
|
||||
compression: CompressionCode::default(),
|
||||
})
|
||||
.expect("failed to send image command");
|
||||
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");
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use crate::transport::Transport;
|
||||
use log::warn;
|
||||
use servicepoint::*;
|
||||
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.");
|
||||
let mut app = App {
|
||||
connection,
|
||||
|
@ -14,7 +15,7 @@ pub(crate) fn stream_stdin(connection: &Connection, slow: bool) {
|
|||
}
|
||||
|
||||
struct App<'t> {
|
||||
connection: &'t Connection,
|
||||
connection: &'t Transport,
|
||||
mirror: CharGrid,
|
||||
y: usize,
|
||||
slow: bool,
|
||||
|
@ -23,7 +24,7 @@ struct App<'t> {
|
|||
impl App<'_> {
|
||||
fn run(&mut self) {
|
||||
self.connection
|
||||
.send(Command::Clear)
|
||||
.send_command(ClearCommand)
|
||||
.expect("couldn't clear screen");
|
||||
let last_y = self.mirror.height() - 1;
|
||||
for line in std::io::stdin().lines() {
|
||||
|
@ -63,10 +64,10 @@ impl App<'_> {
|
|||
|
||||
fn send_mirror(&self) {
|
||||
self.connection
|
||||
.send(Command::Utf8Data(
|
||||
Origin::ZERO,
|
||||
self.mirror.clone(),
|
||||
))
|
||||
.send_command(CharGridCommand {
|
||||
origin: Origin::ZERO,
|
||||
grid: self.mirror.clone(),
|
||||
})
|
||||
.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 self.mirror, self.y, line);
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::{
|
||||
cli::{ImageProcessingOptions, StreamScreenOptions},
|
||||
image_processing::ImageProcessingPipeline,
|
||||
transport::Transport,
|
||||
};
|
||||
use image::{DynamicImage, ImageBuffer, Rgb, Rgba};
|
||||
use log::{debug, error, info, trace, warn};
|
||||
|
@ -9,11 +10,11 @@ use scap::{
|
|||
frame::convert_bgra_to_rgb,
|
||||
frame::Frame,
|
||||
};
|
||||
use servicepoint::{Command, CompressionCode, Connection, Origin, FRAME_PACING};
|
||||
use servicepoint::{BitmapCommand, CompressionCode, Origin, FRAME_PACING};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub fn stream_window(
|
||||
connection: &Connection,
|
||||
connection: &Transport,
|
||||
options: StreamScreenOptions,
|
||||
processing_options: ImageProcessingOptions,
|
||||
) {
|
||||
|
@ -36,11 +37,11 @@ pub fn stream_window(
|
|||
trace!("bitmap ready to send in: {:?}", start.elapsed());
|
||||
|
||||
connection
|
||||
.send(Command::BitmapLinearWin(
|
||||
Origin::ZERO,
|
||||
bitmap.clone(),
|
||||
CompressionCode::default(),
|
||||
))
|
||||
.send_command(BitmapCommand {
|
||||
origin: Origin::ZERO,
|
||||
bitmap: bitmap.clone(),
|
||||
compression: CompressionCode::default(),
|
||||
})
|
||||
.expect("failed to send frame to display");
|
||||
|
||||
debug!("frame time: {:?}", start.elapsed());
|
||||
|
|
10
src/text.rs
10
src/text.rs
|
@ -1,7 +1,7 @@
|
|||
use servicepoint::Connection;
|
||||
use crate::cli::TextCommand;
|
||||
use crate::stream_stdin::stream_stdin;
|
||||
use crate::{cli::TextCommand, stream_stdin::stream_stdin, transport::Transport};
|
||||
|
||||
pub fn text(connection: &Connection, command: TextCommand) {
|
||||
match command { TextCommand::Stdin { slow } => stream_stdin(connection, slow), }
|
||||
pub fn text(connection: &Transport, command: TextCommand) {
|
||||
match command {
|
||||
TextCommand::Stdin { slow } => stream_stdin(connection, slow),
|
||||
}
|
||||
}
|
||||
|
|
51
src/transport.rs
Normal file
51
src/transport.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue