From 51c60250fbb572308ef0126ce8f60f6977046004 Mon Sep 17 00:00:00 2001
From: Vinzenz Schroeter <vinzenz.f.s@gmail.com>
Date: Sun, 4 May 2025 15:29:54 +0200
Subject: [PATCH] reimplement websocket

---
 Cargo.lock            | 217 ++++++++++++++++++++++++++++++++++++++++--
 Cargo.toml            |   1 +
 src/brightness.rs     |   9 +-
 src/cli.rs            |  28 +++---
 src/ledwand_dither.rs |   2 +-
 src/main.rs           |  20 ++--
 src/pixels.rs         |  35 ++++---
 src/stream_stdin.rs   |  10 +-
 src/stream_window.rs  |   6 +-
 src/text.rs           |  10 +-
 src/transport.rs      |  51 ++++++++++
 11 files changed, 327 insertions(+), 62 deletions(-)
 create mode 100644 src/transport.rs

diff --git a/Cargo.lock b/Cargo.lock
index ecab629..142bca9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -204,6 +204,15 @@ version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
 
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
 [[package]]
 name = "built"
 version = "0.7.7"
@@ -234,6 +243,12 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
 
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
 [[package]]
 name = "cc"
 version = "1.2.16"
@@ -440,6 +455,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "crc32fast"
 version = "1.4.2"
@@ -480,6 +504,16 @@ version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
 
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
 [[package]]
 name = "ctrlc"
 version = "3.4.5"
@@ -490,6 +524,12 @@ dependencies = [
  "windows-sys",
 ]
 
+[[package]]
+name = "data-encoding"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
+
 [[package]]
 name = "dbus"
 version = "0.9.7"
@@ -501,6 +541,16 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
 [[package]]
 name = "dispatch"
 version = "0.2.0"
@@ -599,6 +649,12 @@ dependencies = [
  "miniz_oxide",
 ]
 
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
 [[package]]
 name = "foreign-types"
 version = "0.5.0"
@@ -721,6 +777,16 @@ dependencies = [
  "slab",
 ]
 
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.2.15"
@@ -729,7 +795,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
 dependencies = [
  "cfg-if",
  "libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
 ]
 
 [[package]]
@@ -770,6 +848,23 @@ version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
+[[package]]
+name = "http"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
 [[package]]
 name = "humantime"
 version = "2.1.0"
@@ -851,6 +946,12 @@ dependencies = [
  "either",
 ]
 
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
 [[package]]
 name = "jobserver"
 version = "0.1.32"
@@ -1317,6 +1418,12 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "r-efi"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
+
 [[package]]
 name = "radium"
 version = "0.7.0"
@@ -1330,8 +1437,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 dependencies = [
  "libc",
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.3",
 ]
 
 [[package]]
@@ -1341,7 +1458,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
 dependencies = [
  "ppv-lite86",
- "rand_core",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.3",
 ]
 
 [[package]]
@@ -1350,7 +1477,16 @@ version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
 dependencies = [
- "getrandom",
+ "getrandom 0.2.15",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom 0.3.2",
 ]
 
 [[package]]
@@ -1379,8 +1515,8 @@ dependencies = [
  "once_cell",
  "paste",
  "profiling",
- "rand",
- "rand_chacha",
+ "rand 0.8.5",
+ "rand_chacha 0.3.1",
  "simd_helpers",
  "system-deps",
  "thiserror 1.0.69",
@@ -1500,7 +1636,7 @@ dependencies = [
  "dbus",
  "objc",
  "pipewire",
- "rand",
+ "rand 0.8.5",
  "screencapturekit",
  "screencapturekit-sys",
  "sysinfo",
@@ -1591,6 +1727,18 @@ dependencies = [
  "log",
  "scap",
  "servicepoint",
+ "tungstenite",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
 ]
 
 [[package]]
@@ -1783,6 +1931,29 @@ dependencies = [
  "winnow",
 ]
 
+[[package]]
+name = "tungstenite"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
+dependencies = [
+ "bytes",
+ "data-encoding",
+ "http",
+ "httparse",
+ "log",
+ "rand 0.9.1",
+ "sha1",
+ "thiserror 2.0.11",
+ "utf-8",
+]
+
+[[package]]
+name = "typenum"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.17"
@@ -1801,6 +1972,12 @@ version = "0.1.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
 
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
 [[package]]
 name = "utf8parse"
 version = "0.2.2"
@@ -1830,12 +2007,27 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
 
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
 [[package]]
 name = "wasi"
 version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
+[[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
 [[package]]
 name = "wasm-bindgen"
 version = "0.2.100"
@@ -2101,6 +2293,15 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags 2.8.0",
+]
+
 [[package]]
 name = "wyz"
 version = "0.5.1"
diff --git a/Cargo.toml b/Cargo.toml
index 08994a2..f1f2266 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,6 +19,7 @@ log = "0.4"
 scap = "0.0.8"
 image = "0.25.5"
 fast_image_resize = { version = "5.1.2", features = ["image"] }
+tungstenite = "0.26"
 
 [dependencies.servicepoint]
 package = "servicepoint"
diff --git a/src/brightness.rs b/src/brightness.rs
index 469727f..49ad898 100644
--- a/src/brightness.rs
+++ b/src/brightness.rs
@@ -1,9 +1,8 @@
-use std::net::UdpSocket;
-use crate::cli::BrightnessCommand;
+use crate::{cli::BrightnessCommand, transport::Transport};
 use log::info;
-use servicepoint::{Brightness, GlobalBrightnessCommand, UdpSocketExt};
+use servicepoint::{Brightness, GlobalBrightnessCommand};
 
-pub(crate) fn brightness(connection: &UdpSocket, 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),
@@ -13,7 +12,7 @@ pub(crate) fn brightness(connection: &UdpSocket, brightness_command: BrightnessC
     }
 }
 
-pub(crate) fn brightness_set(connection: &UdpSocket, brightness: Brightness) {
+pub(crate) fn brightness_set(connection: &Transport, brightness: Brightness) {
     connection
         .send_command(GlobalBrightnessCommand::from(brightness))
         .expect("Failed to set brightness");
diff --git a/src/cli.rs b/src/cli.rs
index 0ca554e..543ba36 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -12,14 +12,14 @@ pub struct Cli {
         default_value = "127.0.0.1:2342"
     )]
     pub destination: String,
-    //#[arg(
-    //    short,
-    //    long,
-    //    help = "protocol to use for communication with display",
-    //    value_enum,
-    //    default_value = "udp"
-    //)]
-    //pub transport: Protocol,
+    #[arg(
+        short,
+        long,
+        help = "protocol to use for communication with display",
+        value_enum,
+        default_value = "udp"
+    )]
+    pub transport: TransportType,
     #[clap(subcommand)]
     pub command: Mode,
     #[clap(short, long, help = "verbose logging")]
@@ -106,12 +106,12 @@ pub enum BrightnessCommand {
     Min,
 }
 
-//#[derive(clap::ValueEnum, Clone, Debug)]
-//pub enum Protocol {
-//    Udp,
-//    WebSocket,
-//    Fake,
-//}
+#[derive(clap::ValueEnum, Clone, Debug)]
+pub enum TransportType {
+    Udp,
+    WebSocket,
+    Fake,
+}
 
 #[derive(clap::Parser, std::fmt::Debug)]
 #[clap(about = "Commands for sending text to the screen")]
diff --git a/src/ledwand_dither.rs b/src/ledwand_dither.rs
index 61a3d3c..95ffe02 100644
--- a/src/ledwand_dither.rs
+++ b/src/ledwand_dither.rs
@@ -1,7 +1,7 @@
 //! Based on https://github.com/WarkerAnhaltRanger/CCCB_Ledwand
 
 use image::GrayImage;
-use servicepoint::{DisplayBitVec, Bitmap, PIXEL_HEIGHT};
+use servicepoint::{Bitmap, DisplayBitVec, PIXEL_HEIGHT};
 
 type GrayHistogram = [usize; 256];
 
diff --git a/src/main.rs b/src/main.rs
index dd32a9e..cdae13c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,13 +1,13 @@
-use std::net::UdpSocket;
 use crate::{
     brightness::{brightness, brightness_set},
     cli::{Cli, Mode},
     pixels::{pixels, pixels_off},
-    text::text
+    text::text,
+    transport::Transport,
 };
 use clap::Parser;
 use log::debug;
-use servicepoint::{Brightness, HardResetCommand, UdpSocketExt};
+use servicepoint::{Brightness, HardResetCommand};
 
 mod brightness;
 mod cli;
@@ -17,22 +17,22 @@ 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 = UdpSocket::bind_connect(cli.destination)
-        .expect("Failed to connect");
-    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: UdpSocket) {
+pub fn execute_mode(mode: Mode, connection: Transport) {
     match mode {
-        Mode::Reset{force} => {
+        Mode::Reset { force } => {
             if force {
                 connection.send_command(HardResetCommand).unwrap()
             } else {
@@ -42,7 +42,7 @@ pub fn execute_mode(mode: Mode, connection: UdpSocket) {
         }
         Mode::Pixels { pixel_command } => pixels(&connection, pixel_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),
     }
 }
 
diff --git a/src/pixels.rs b/src/pixels.rs
index d22b39a..a7aad66 100644
--- a/src/pixels.rs
+++ b/src/pixels.rs
@@ -1,13 +1,16 @@
-use std::net::UdpSocket;
 use crate::{
-    image_processing::ImageProcessingPipeline,
     cli::{ImageProcessingOptions, PixelCommand, SendImageOptions},
-    stream_window::stream_window
+    image_processing::ImageProcessingPipeline,
+    stream_window::stream_window,
+    transport::Transport,
 };
 use log::info;
-use servicepoint::{BinaryOperation, BitVecCommand, UdpSocketExt, BitmapCommand, ClearCommand, CompressionCode, DisplayBitVec, Origin, PIXEL_COUNT};
+use servicepoint::{
+    BinaryOperation, BitVecCommand, BitmapCommand, ClearCommand, CompressionCode, DisplayBitVec,
+    Origin, PIXEL_COUNT,
+};
 
-pub(crate) fn pixels(connection: &UdpSocket, 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),
@@ -23,25 +26,35 @@ pub(crate) fn pixels(connection: &UdpSocket, pixel_command: PixelCommand) {
     }
 }
 
-fn pixels_on(connection: &UdpSocket) {
+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};
+    let command = BitVecCommand {
+        offset: 0,
+        bitvec: mask,
+        compression: CompressionCode::Lzma,
+        operation: BinaryOperation::Overwrite,
+    };
     connection
         .send_command(command)
         .expect("could not send command");
     info!("turned on all pixels")
 }
 
-fn pixels_invert(connection: &UdpSocket) {
+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};
+    let command = BitVecCommand {
+        offset: 0,
+        bitvec: mask,
+        compression: CompressionCode::Lzma,
+        operation: BinaryOperation::Xor,
+    };
     connection
         .send_command(command)
         .expect("could not send command");
     info!("inverted all pixels");
 }
 
-pub(crate) fn pixels_off(connection: &UdpSocket) {
+pub(crate) fn pixels_off(connection: &Transport) {
     connection
         .send_command(ClearCommand)
         .expect("failed to clear pixels");
@@ -49,7 +62,7 @@ pub(crate) fn pixels_off(connection: &UdpSocket) {
 }
 
 fn pixels_image(
-    connection: &UdpSocket,
+    connection: &Transport,
     options: SendImageOptions,
     processing_options: ImageProcessingOptions,
 ) {
diff --git a/src/stream_stdin.rs b/src/stream_stdin.rs
index 5980b4b..872461c 100644
--- a/src/stream_stdin.rs
+++ b/src/stream_stdin.rs
@@ -1,9 +1,9 @@
-use std::net::UdpSocket;
+use crate::transport::Transport;
 use log::warn;
 use servicepoint::*;
 use std::thread::sleep;
 
-pub(crate) fn stream_stdin(connection: &UdpSocket, 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,
@@ -15,7 +15,7 @@ pub(crate) fn stream_stdin(connection: &UdpSocket, slow: bool) {
 }
 
 struct App<'t> {
-    connection: &'t UdpSocket,
+    connection: &'t Transport,
     mirror: CharGrid,
     y: usize,
     slow: bool,
@@ -78,8 +78,8 @@ impl App<'_> {
         Self::line_onto_grid(&mut self.mirror, self.y, line);
         self.connection
             .send_command(CharGridCommand {
-                origin: Origin::new(0, self.y), 
-                grid: line_grid
+                origin: Origin::new(0, self.y),
+                grid: line_grid,
             })
             .expect("couldn't send single line to screen");
     }
diff --git a/src/stream_window.rs b/src/stream_window.rs
index 971e12d..ffbc717 100644
--- a/src/stream_window.rs
+++ b/src/stream_window.rs
@@ -1,7 +1,7 @@
-use std::net::UdpSocket;
 use crate::{
     cli::{ImageProcessingOptions, StreamScreenOptions},
     image_processing::ImageProcessingPipeline,
+    transport::Transport,
 };
 use image::{DynamicImage, ImageBuffer, Rgb, Rgba};
 use log::{debug, error, info, trace, warn};
@@ -10,11 +10,11 @@ use scap::{
     frame::convert_bgra_to_rgb,
     frame::Frame,
 };
-use servicepoint::{BitmapCommand, CompressionCode, Origin, UdpSocketExt, FRAME_PACING};
+use servicepoint::{BitmapCommand, CompressionCode, Origin, FRAME_PACING};
 use std::time::{Duration, Instant};
 
 pub fn stream_window(
-    connection: &UdpSocket,
+    connection: &Transport,
     options: StreamScreenOptions,
     processing_options: ImageProcessingOptions,
 ) {
diff --git a/src/text.rs b/src/text.rs
index c7ddb8a..b5fbd4c 100644
--- a/src/text.rs
+++ b/src/text.rs
@@ -1,7 +1,7 @@
-use std::net::UdpSocket;
-use crate::cli::TextCommand;
-use crate::stream_stdin::stream_stdin;
+use crate::{cli::TextCommand, stream_stdin::stream_stdin, transport::Transport};
 
-pub fn text(connection: &UdpSocket, 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),
+    }
 }
diff --git a/src/transport.rs b/src/transport.rs
new file mode 100644
index 0000000..03c651a
--- /dev/null
+++ b/src/transport.rs
@@ -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),
+        }
+    }
+}