diff --git a/Cargo.lock b/Cargo.lock
index 8baac5943837069caf73b01109262ccad8d13fd0..26f8f7ce41035bcabee88c776c64560c48606a50 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -23,6 +23,12 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "atomic_refcell"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79d6dc922a2792b006573f60b2648076355daeae5ce9cb59507e5908c9625d31"
+
 [[package]]
 name = "autocfg"
 version = "1.1.0"
@@ -38,6 +44,9 @@ dependencies = [
  "base64",
  "bitvec",
  "crc",
+ "gstreamer",
+ "gstreamer-app",
+ "gstreamer-video",
  "image",
  "kiss-tnc",
  "rppal",
@@ -66,6 +75,12 @@ version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
+[[package]]
+name = "bitflags"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
+
 [[package]]
 name = "bitvec"
 version = "1.0.1"
@@ -111,6 +126,16 @@ version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
 
+[[package]]
+name = "cfg-expr"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b40ccee03b5175c18cde8f37e7d2a33bcef6f8ec8f7cc0d81090d1bb380949c9"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
 [[package]]
 name = "cfg-if"
 version = "1.0.0"
@@ -285,18 +310,69 @@ version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
 
+[[package]]
+name = "futures-channel"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
+dependencies = [
+ "futures-core",
+]
+
 [[package]]
 name = "futures-core"
 version = "0.3.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
 
+[[package]]
+name = "futures-executor"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.18",
+]
+
 [[package]]
 name = "futures-sink"
 version = "0.3.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
 
+[[package]]
+name = "futures-task"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
+
+[[package]]
+name = "futures-util"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
+dependencies = [
+ "futures-core",
+ "futures-macro",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
 [[package]]
 name = "generic-array"
 version = "0.14.7"
@@ -330,6 +406,197 @@ dependencies = [
  "weezl",
 ]
 
+[[package]]
+name = "gio-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "winapi",
+]
+
+[[package]]
+name = "glib"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "331156127e8166dd815cf8d2db3a5beb492610c716c03ee6db4f2d07092af0a7"
+dependencies = [
+ "bitflags 2.4.0",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-task",
+ "futures-util",
+ "gio-sys",
+ "glib-macros",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "memchr",
+ "once_cell",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "179643c50bf28d20d2f6eacd2531a88f2f5d9747dd0b86b8af1e8bb5dd0de3c0"
+dependencies = [
+ "heck",
+ "proc-macro-crate",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.18",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gobject-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gstreamer"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8cdb86791dc39a5443f7d08cf3e7ae9c88a94991aba620d177cb5804838201f"
+dependencies = [
+ "cfg-if",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "glib",
+ "gstreamer-sys",
+ "itertools",
+ "libc",
+ "muldiv",
+ "num-integer",
+ "num-rational",
+ "option-operations",
+ "paste",
+ "pretty-hex",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gstreamer-app"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6da0a8e5671d836ac70741dff0a3e7688ec9ac7ab8dc62c3135dece85df63d4"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "glib",
+ "gstreamer",
+ "gstreamer-app-sys",
+ "gstreamer-base",
+ "libc",
+]
+
+[[package]]
+name = "gstreamer-app-sys"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bb093f39bd9b3d0c4ef8f1c4d46028d35bce033f9bda1a0449e21ef17349e03"
+dependencies = [
+ "glib-sys",
+ "gstreamer-base-sys",
+ "gstreamer-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gstreamer-base"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fe38a6d5c1e516ce3fd6069e972a540d315448ed69fdadad739e6c6c6eb2a01"
+dependencies = [
+ "atomic_refcell",
+ "cfg-if",
+ "glib",
+ "gstreamer",
+ "gstreamer-base-sys",
+ "libc",
+]
+
+[[package]]
+name = "gstreamer-base-sys"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b9c029583ed61fa5258076a42df91732dc7f5582044ea7ee66a721641e6af4"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "gstreamer-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gstreamer-sys"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a70e3a99118bcd1221f8a62d7a905bae5e5cc2cda678bb46bf3cd36e0f899d33"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gstreamer-video"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0db8adfc000cd58f8ece0fe6b4beb79e19e4a6135cfb81138fdb016b603f7d60"
+dependencies = [
+ "cfg-if",
+ "futures-channel",
+ "glib",
+ "gstreamer",
+ "gstreamer-base",
+ "gstreamer-video-sys",
+ "libc",
+]
+
+[[package]]
+name = "gstreamer-video-sys"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0284250a09fa824b21df1a21967eef4a5d85b5e0c1e335ed2ba9b9be1424dae"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "gstreamer-base-sys",
+ "gstreamer-sys",
+ "libc",
+ "system-deps",
+]
+
 [[package]]
 name = "half"
 version = "2.2.1"
@@ -345,6 +612,12 @@ version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
 
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
 [[package]]
 name = "hermit-abi"
 version = "0.2.6"
@@ -383,6 +656,15 @@ dependencies = [
  "hashbrown",
 ]
 
+[[package]]
+name = "itertools"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
+dependencies = [
+ "either",
+]
+
 [[package]]
 name = "jpeg-decoder"
 version = "0.3.0"
@@ -497,6 +779,12 @@ dependencies = [
  "windows-sys 0.45.0",
 ]
 
+[[package]]
+name = "muldiv"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0"
+
 [[package]]
 name = "nanorand"
 version = "0.7.0"
@@ -552,6 +840,15 @@ version = "1.17.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
 
+[[package]]
+name = "option-operations"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0"
+dependencies = [
+ "paste",
+]
+
 [[package]]
 name = "parking_lot"
 version = "0.12.1"
@@ -575,6 +872,12 @@ dependencies = [
  "windows-sys 0.45.0",
 ]
 
+[[package]]
+name = "paste"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
+
 [[package]]
 name = "pin-project"
 version = "1.1.0"
@@ -592,7 +895,7 @@ checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.18",
 ]
 
 [[package]]
@@ -601,19 +904,71 @@ version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
 
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
+
 [[package]]
 name = "png"
 version = "0.17.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "aaeebc51f9e7d2c150d3f3bfeb667f2aa985db5ef1e3d212847bdedb488beeaa"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
  "crc32fast",
  "fdeflate",
  "flate2",
  "miniz_oxide 0.7.1",
 ]
 
+[[package]]
+name = "pretty-hex"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5"
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.59"
@@ -675,7 +1030,7 @@ version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
 ]
 
 [[package]]
@@ -710,7 +1065,7 @@ checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.18",
 ]
 
 [[package]]
@@ -747,6 +1102,15 @@ version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f"
 
+[[package]]
+name = "slab"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "smallvec"
 version = "1.10.0"
@@ -782,6 +1146,16 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "unicode-ident",
+]
+
 [[package]]
 name = "syn"
 version = "2.0.18"
@@ -793,12 +1167,31 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "system-deps"
+version = "6.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3"
+dependencies = [
+ "cfg-expr",
+ "heck",
+ "pkg-config",
+ "toml",
+ "version-compare",
+]
+
 [[package]]
 name = "tap"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
 
+[[package]]
+name = "target-lexicon"
+version = "0.12.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a"
+
 [[package]]
 name = "thiserror"
 version = "1.0.40"
@@ -816,7 +1209,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.18",
 ]
 
 [[package]]
@@ -857,7 +1250,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.18",
 ]
 
 [[package]]
@@ -906,6 +1299,12 @@ version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
 
+[[package]]
+name = "version-compare"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
+
 [[package]]
 name = "version_check"
 version = "0.9.4"
@@ -939,7 +1338,7 @@ dependencies = [
  "once_cell",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.18",
  "wasm-bindgen-shared",
 ]
 
@@ -961,7 +1360,7 @@ checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.18",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 660f698da87ad1ad489957d25c0acff92da4c559..af841296226f23d59e58d27198a6b36658bed5ca 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,3 +29,6 @@ sha3 = "0.10.7"
 subprocess = "0.2.9"
 tokio = { version = "1.27.0", features = ["full"] }
 toml = "0.7.3"
+gstreamer = "0.21.0"
+gstreamer-app = "0.21.0"
+gstreamer-video = "0.21.0"
diff --git a/debian/maintainer_scripts/postinst b/debian/maintainer_scripts/postinst
index 3d2983f184eabebd15974ddc262b2b19cc018713..6a14146065ab45001e1bd5fc9b80644120a17a5f 100644
--- a/debian/maintainer_scripts/postinst
+++ b/debian/maintainer_scripts/postinst
@@ -1,3 +1,5 @@
 adduser --system --system balloon
 usermod -G gpio -a balloon
 usermod -G dialout -a balloon
+usermod -G spi -a balloon
+usermod -G video -a balloon
diff --git a/src/control.rs b/src/control.rs
index ef709de4536cccaf30ab3f5705c4b2f066c0d371..994ada1acb14709f9c8f7b22f1c1b4f5295309b2 100644
--- a/src/control.rs
+++ b/src/control.rs
@@ -10,12 +10,14 @@ use crate::{
     aprs::CommandHandler,
     config::Config,
     img::ImgManager,
-    packet::{FecPacket, Packet},
+    packet::{FecPacket, Packet, RawPacket},
     radio::McuRadio,
     ssdv::ssdv_encode,
+    video::{start_video, VideoPacker},
 };
 
 const IMAGE_PACKET_QUEUE_LENGTH: usize = 8192;
+const VIDEO_PACKET_QUEUE_LENGTH: usize = 1024;
 const TEMP_REFRESH_INTERVAL: Duration = Duration::from_secs(5);
 
 pub struct Controller {
@@ -29,12 +31,13 @@ impl Controller {
 
     pub fn run_forever(self) {
         let (img_tx, img_rx) = mpsc::sync_channel(IMAGE_PACKET_QUEUE_LENGTH);
+        let (vid_tx, vid_rx) = mpsc::sync_channel(VIDEO_PACKET_QUEUE_LENGTH);
         let (telem_tx, telem_rx) = mpsc::channel();
         let (cmd_tx, cmd_rx) = mpsc::channel();
 
         {
             let callsign = self.config.callsign.clone();
-            thread::spawn(|| Self::tx_thread(callsign, img_rx, telem_rx));
+            thread::spawn(|| Self::tx_thread(callsign, img_rx, vid_rx, telem_rx));
         }
 
         {
@@ -42,6 +45,10 @@ impl Controller {
             thread::spawn(|| Self::aprs_thread(config, cmd_tx, telem_tx));
         }
 
+        {
+            start_video(vid_tx);
+        }
+
         let mut manager = ImgManager::new(self.config.paths.clone(), cmd_rx);
 
         loop {
@@ -84,7 +91,12 @@ impl Controller {
     }
 
     // manages our transceiver
-    fn tx_thread(callsign: String, image_rx: Receiver<FecPacket>, telem_rx: Receiver<Packet>) {
+    fn tx_thread(
+        callsign: String,
+        image_rx: Receiver<FecPacket>,
+        vid_rx: Receiver<Vec<u8>>,
+        telem_rx: Receiver<Packet>,
+    ) {
         let mut radio = loop {
             let r = McuRadio::new();
 
@@ -99,14 +111,17 @@ impl Controller {
         };
 
         let mut text_msg_id = 0;
+        let mut video_packer = VideoPacker::new();
         let mut last_got_temp = Instant::now();
         loop {
             if let Err(e) = Self::tx_thread_single_iter(
                 &callsign,
                 &image_rx,
+                &vid_rx,
                 &telem_rx,
                 &mut text_msg_id,
                 &mut last_got_temp,
+                &mut video_packer,
                 &mut radio,
             ) {
                 eprintln!("Radio error: {}", e);
@@ -115,12 +130,16 @@ impl Controller {
         }
     }
 
+    // TODO this should probably be a struct
+    #[allow(clippy::too_many_arguments)]
     fn tx_thread_single_iter(
         callsign: &str,
         image_rx: &Receiver<FecPacket>,
+        vid_rx: &Receiver<Vec<u8>>,
         telem_rx: &Receiver<Packet>,
         text_msg_id: &mut u16,
         last_got_temp: &mut Instant,
+        video_packer: &mut VideoPacker,
         radio: &mut McuRadio,
     ) -> anyhow::Result<()> {
         while let Ok(tm) = telem_rx.try_recv() {
@@ -129,12 +148,32 @@ impl Controller {
             radio.send_packet(&tm)?;
         }
 
+        while let Ok(vid) = vid_rx.try_recv() {
+            video_packer.pack(&vid, |pkt| radio.send_packet(&pkt.into()))?;
+        }
+
         if let Ok(img) = image_rx.try_recv() {
             radio.send_packet(&img)?;
         } else {
-            radio.flush()?;
-
-            thread::sleep(Duration::from_millis(50));
+            // send garbage. We want our radio to be active at all times, for AGC purposes on the downlink side
+            const GARBAGE: [u8; 256] = [
+                102, 54, 91, 67, 89, 223, 198, 83, 22, 16, 48, 184, 117, 97, 153, 246, 169, 167,
+                89, 85, 49, 67, 128, 123, 83, 210, 58, 104, 248, 102, 219, 195, 121, 57, 101, 172,
+                57, 223, 190, 106, 90, 36, 39, 156, 99, 92, 87, 10, 56, 29, 137, 71, 144, 89, 82,
+                182, 127, 72, 93, 249, 214, 6, 155, 164, 177, 22, 84, 111, 52, 60, 68, 235, 30, 13,
+                174, 101, 49, 43, 95, 61, 214, 89, 110, 24, 77, 208, 103, 209, 87, 12, 218, 147,
+                224, 85, 178, 49, 28, 233, 65, 132, 61, 238, 70, 164, 177, 90, 158, 99, 180, 77,
+                251, 17, 227, 43, 109, 33, 120, 15, 89, 172, 69, 213, 25, 166, 59, 254, 220, 31,
+                21, 247, 246, 12, 204, 223, 134, 136, 100, 92, 20, 182, 204, 79, 239, 120, 8, 40,
+                138, 222, 239, 85, 15, 196, 169, 36, 38, 193, 207, 165, 7, 4, 33, 4, 120, 250, 114,
+                240, 128, 3, 22, 62, 254, 139, 13, 56, 153, 15, 63, 96, 62, 44, 128, 241, 25, 22,
+                125, 127, 0, 137, 165, 145, 156, 39, 90, 94, 145, 86, 156, 17, 187, 217, 249, 193,
+                112, 160, 238, 216, 183, 46, 27, 74, 38, 127, 233, 188, 184, 35, 194, 249, 90, 195,
+                33, 21, 67, 56, 75, 243, 140, 6, 187, 93, 49, 224, 20, 34, 204, 204, 141, 132, 252,
+                101, 3, 149, 107, 173, 139, 125, 41, 133, 251, 42, 171, 130, 254, 145, 34, 56,
+            ];
+
+            radio.send_packet(&RawPacket(GARBAGE).into())?;
         }
 
         if Instant::now() - *last_got_temp > TEMP_REFRESH_INTERVAL {
diff --git a/src/main.rs b/src/main.rs
index 168e135c6c7a0fd5889128fd56fd2d06e32640b9..a67e1528a1413b21bb3baccc0edb2dd1e81b1c8d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -10,6 +10,7 @@ mod ldpc;
 mod packet;
 mod radio;
 mod ssdv;
+mod video;
 
 fn main() -> anyhow::Result<()> {
     let config = Config::load()?;
diff --git a/src/packet.rs b/src/packet.rs
index 1d52ce6bf13a224b9cb761911a346e8b367f7178..d7f3f9c199f5459c5e7bc397a619ba9e04306402 100644
--- a/src/packet.rs
+++ b/src/packet.rs
@@ -3,10 +3,12 @@ use crc::{Crc, CRC_16_IBM_3740};
 use crate::ldpc::ldpc_encode;
 
 const TEXT_MESSAGE_LEN: usize = 252;
+pub const VIDEO_LEN: usize = 255;
 const FEC_PACKET_LEN: usize = 256 + 2 + 65;
 
 pub enum Packet {
     TextMessage(u8, [u8; TEXT_MESSAGE_LEN]),
+    Video([u8; VIDEO_LEN]),
 }
 
 impl Packet {
@@ -33,6 +35,10 @@ impl Packet {
 
         Self::TextMessage(len, out)
     }
+
+    pub fn new_video(data: [u8; VIDEO_LEN]) -> Self {
+        Self::Video(data)
+    }
 }
 
 impl Packet {
@@ -52,12 +58,21 @@ impl Packet {
 
                 RawPacket(out)
             }
+
+            Packet::Video(data) => {
+                let mut out = [0; 256];
+                out[0] = 0x04; // packet type
+                out[1..].clone_from_slice(&data);
+
+                RawPacket(out)
+            }
         }
     }
 }
 
 pub struct RawPacket(pub [u8; 256]);
 
+#[derive(Debug)]
 pub struct FecPacket(pub [u8; FEC_PACKET_LEN]);
 
 impl From<RawPacket> for FecPacket {
diff --git a/src/radio.rs b/src/radio.rs
index 6a35abfd577874854d3dac7667cbf3307b7378b7..4e22315a4ab542837debf5dc41aed51d33638c9e 100644
--- a/src/radio.rs
+++ b/src/radio.rs
@@ -57,13 +57,6 @@ impl McuRadio {
         Ok(self.get_info()?.1)
     }
 
-    // sends a bunch of nops over SPI to clear out its DMA buffer
-    pub fn flush(&mut self) -> anyhow::Result<()> {
-        self.spi.write(&[NOP_CMD; 256])?;
-
-        Ok(())
-    }
-
     // try 10 times and then give up
     fn get_info(&mut self) -> anyhow::Result<(bool, f32)> {
         for _ in 0..9 {
@@ -95,6 +88,7 @@ impl McuRadio {
                 return Ok((free, temp));
             }
         }
+
         Err(anyhow!("could not find valid data"))
     }
 }
diff --git a/src/video.rs b/src/video.rs
new file mode 100644
index 0000000000000000000000000000000000000000..4af4e30bc352ce105ce2daa4965deb67535f9437
--- /dev/null
+++ b/src/video.rs
@@ -0,0 +1,222 @@
+use std::{
+    sync::mpsc::{SyncSender, TrySendError},
+    thread,
+    time::Duration,
+};
+
+use crate::packet::{Packet, RawPacket, VIDEO_LEN};
+use anyhow::Context;
+use gstreamer::{
+    element_error,
+    prelude::{Cast, ElementExtManual, GstBinExtManual},
+    traits::{ElementExt, GstObjectExt},
+    Bus, Caps, ClockTime, FlowError, FlowSuccess, Pipeline, ResourceError, State,
+};
+use gstreamer_app::{AppSink, AppSinkCallbacks};
+use gstreamer_video::{VideoCapsBuilder, VideoFormat};
+
+const DATA_WHITENER: [u8; 255] = [
+    102, 54, 91, 67, 89, 223, 198, 83, 22, 16, 48, 184, 117, 97, 153, 246, 169, 167, 89, 85, 49,
+    67, 128, 123, 83, 210, 58, 104, 248, 102, 219, 195, 121, 57, 101, 172, 57, 223, 190, 106, 90,
+    36, 39, 156, 99, 92, 87, 10, 56, 29, 137, 71, 144, 89, 82, 182, 127, 72, 93, 249, 214, 6, 155,
+    164, 177, 22, 84, 111, 52, 60, 68, 235, 30, 13, 174, 101, 49, 43, 95, 61, 214, 89, 110, 24, 77,
+    208, 103, 209, 87, 12, 218, 147, 224, 85, 178, 49, 28, 233, 65, 132, 61, 238, 70, 164, 177, 90,
+    158, 99, 180, 77, 251, 17, 227, 43, 109, 33, 120, 15, 89, 172, 69, 213, 25, 166, 59, 254, 220,
+    31, 21, 247, 246, 12, 204, 223, 134, 136, 100, 92, 20, 182, 204, 79, 239, 120, 8, 40, 138, 222,
+    239, 85, 15, 196, 169, 36, 38, 193, 207, 165, 7, 4, 33, 4, 120, 250, 114, 240, 128, 3, 22, 62,
+    254, 139, 13, 56, 153, 15, 63, 96, 62, 44, 128, 241, 25, 22, 125, 127, 0, 137, 165, 145, 156,
+    39, 90, 94, 145, 86, 156, 17, 187, 217, 249, 193, 112, 160, 238, 216, 183, 46, 27, 74, 38, 127,
+    233, 188, 184, 35, 194, 249, 90, 195, 33, 21, 67, 56, 75, 243, 140, 6, 187, 93, 49, 224, 20,
+    34, 204, 204, 141, 132, 252, 101, 3, 149, 107, 173, 139, 125, 41, 133, 251, 42, 171, 130, 254,
+    145, 34,
+];
+
+pub struct VideoPacker {
+    buf: [u8; VIDEO_LEN],
+    buf_i: usize,
+}
+
+impl VideoPacker {
+    pub fn new() -> Self {
+        Self {
+            buf: [0; VIDEO_LEN],
+            buf_i: 0,
+        }
+    }
+
+    // theoretically suboptimal. If one packet fails to send the loop terminates and we throw away all the passed in data
+    pub fn pack<E, F>(&mut self, data: &[u8], mut f: F) -> Result<(), E>
+    where
+        F: FnMut(RawPacket) -> Result<(), E>,
+    {
+        for d in data {
+            self.buf[self.buf_i] = *d;
+            self.buf_i += 1;
+
+            if self.buf_i >= VIDEO_LEN {
+                // data whitening
+                // makes our SDR happy
+                for (b, w) in self.buf.iter_mut().zip(DATA_WHITENER.iter()) {
+                    *b ^= w;
+                }
+
+                let pkt = Packet::new_video(self.buf);
+                self.buf_i = 0;
+
+                // very garbage. we only pass in a text id here
+                // because the function requires it. it's not used
+                f(pkt.into_raw(&mut 0))?;
+            }
+        }
+
+        Ok(())
+    }
+}
+
+pub fn start_video(sender: SyncSender<Vec<u8>>) {
+    thread::spawn(move || loop {
+        match init(sender.clone()) {
+            Ok((pipeline, bus)) => handle_pipeline(pipeline, bus),
+            Err(e) => {
+                eprintln!("Could not restart video pipeline: {e:?}")
+            }
+        }
+
+        thread::sleep(Duration::from_secs(1));
+    });
+}
+
+fn init(sender: SyncSender<Vec<u8>>) -> anyhow::Result<(Pipeline, Bus)> {
+    gstreamer::init()?;
+
+    let src = gstreamer::ElementFactory::make("libcamerasrc")
+        .name("source")
+        .build()
+        .context("Could not create source element")?;
+
+    let capsfilter = gstreamer::ElementFactory::make("capsfilter")
+        .property(
+            "caps",
+            VideoCapsBuilder::new()
+                .width(640)
+                .height(480)
+                .framerate((12, 1).into())
+                .format(VideoFormat::Nv21)
+                .build(),
+        )
+        .build()
+        .context("Could not build capsfilter")?;
+
+    let conv = gstreamer::ElementFactory::make("videoconvert")
+        .name("conv")
+        .build()
+        .context("Could not build converter")?;
+
+    let enc = gstreamer::ElementFactory::make("x265enc")
+        .name("enc")
+        .property("bitrate", 200u32)
+        .property("key-int-max", 48)
+        .property_from_str("speed-preset", "ultrafast")
+        .build()
+        .context("Could not build encoder")?;
+
+    let parse = gstreamer::ElementFactory::make("h265parse")
+        .name("parse")
+        .property("config-interval", -1)
+        .build()
+        .context("Could not build parser")?;
+
+    let mpegts = gstreamer::ElementFactory::make("mpegtsmux")
+        .name("mpegtsmux")
+        .build()
+        .context("Could not create mpegts element")?;
+
+    let appsink = AppSink::builder().caps(&Caps::new_any()).build();
+
+    let pipeline = gstreamer::Pipeline::with_name("pipeline");
+    pipeline.add_many([
+        &src,
+        &capsfilter,
+        &conv,
+        &enc,
+        &parse,
+        &mpegts,
+        appsink.upcast_ref(),
+    ])?;
+    src.link(&capsfilter)?;
+    capsfilter.link(&conv)?;
+    conv.link(&enc)?;
+    enc.link(&parse)?;
+    parse.link(&mpegts)?;
+    mpegts.link(&appsink)?;
+
+    appsink.set_callbacks(
+        AppSinkCallbacks::builder()
+            .new_sample(move |appsink| {
+                let sample = appsink.pull_sample().map_err(|_| FlowError::Eos)?;
+                let buffer = sample.buffer().ok_or_else(|| {
+                    element_error!(
+                        appsink,
+                        ResourceError::Failed,
+                        ("Failed to get buffer from appsink")
+                    );
+
+                    FlowError::Error
+                })?;
+
+                let map = buffer
+                    .map_readable()
+                    .map_err(|_| -> FlowError { FlowError::Error })?;
+
+                match sender.try_send(map.as_ref().to_vec()) {
+                    Ok(_) => {}
+                    Err(TrySendError::Full(_)) => {
+                        eprintln!("Video buffer overrun. Skipping frames");
+                    }
+                    Err(TrySendError::Disconnected(_)) => {
+                        element_error!(appsink, ResourceError::Failed, ("Channel disconnected"));
+
+                        return Err(FlowError::Error);
+                    }
+                }
+
+                Ok(FlowSuccess::Ok)
+            })
+            .build(),
+    );
+
+    pipeline
+        .set_state(State::Playing)
+        .context("Unable to set the pipeline to the `Playing` state")?;
+
+    let bus = pipeline.bus().context("No pipeline bus")?;
+
+    Ok((pipeline, bus))
+}
+
+fn handle_pipeline(pipeline: Pipeline, bus: Bus) {
+    // Wait until error or EOS
+
+    for msg in bus.iter_timed(ClockTime::NONE) {
+        use gstreamer::MessageView;
+
+        match msg.view() {
+            MessageView::Eos(..) => break,
+            MessageView::Error(err) => {
+                eprintln!(
+                    "Error from {:?}: {} ({:?})",
+                    err.src().map(|s| s.path_string()),
+                    err.error(),
+                    err.debug()
+                );
+                break;
+            }
+            _ => (),
+        }
+    }
+
+    // Shutdown pipeline
+    if let Err(e) = pipeline.set_state(State::Null) {
+        println!("Unable to set the pipeline to the `Null` state: {e}");
+    }
+}