Stream video files, update servicepoint, fix dithering #3
					 16 changed files with 677 additions and 286 deletions
				
			
		
							
								
								
									
										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 | ||||
|  |  | |||
							
								
								
									
										62
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										62
									
								
								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,15 +39,15 @@ cargo run -- <args> | |||
| 
 | ||||
| ## Usage | ||||
| 
 | ||||
| ``` | ||||
| ```text | ||||
| Usage: servicepoint-cli [OPTIONS] <COMMAND> | ||||
| 
 | ||||
| Commands: | ||||
|   reset-everything  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] | ||||
|   help              Print this message or the help of the given subcommand(s) | ||||
|   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] | ||||
|   help        Print this message or the help of the given subcommand(s) | ||||
| 
 | ||||
| Options: | ||||
|   -d, --destination <DESTINATION>  ip:port of the servicepoint display [default: 127.0.0.1:2342] | ||||
|  | @ -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
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								flake.lock
									
										
									
										generated
									
									
									
								
							|  | @ -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; | ||||
|         source[to] = diffuse_value.clamp(u8::MIN.into(), u8::MAX.into()) as u8; | ||||
|         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"); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										41
									
								
								src/main.rs
									
										
									
									
									
								
							
							
						
						
									
										41
									
								
								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,40 +17,32 @@ 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 => { | ||||
|             brightness_set(&connection, Brightness::MAX); | ||||
|             pixels_off(&connection); | ||||
|         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, | ||||
|         Mode::Text { text_command } => text(&connection, text_command), | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										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…
	
	Add table
		Add a link
		
	
		Reference in a new issue