diff --git a/Cargo.lock b/Cargo.lock
index 71b460c926..89f2cc83d4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -717,9 +717,9 @@ dependencies = [
 
 [[package]]
 name = "brotli"
-version = "8.0.1"
+version = "8.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d"
+checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
 dependencies = [
  "alloc-no-stdlib",
  "alloc-stdlib",
@@ -829,9 +829,9 @@ dependencies = [
 
 [[package]]
 name = "camino"
-version = "1.1.10"
+version = "1.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab"
+checksum = "5d07aa9a93b00c76f71bc35d598bed923f6d4f3a9ca5c24b7737ae1a292841c0"
 dependencies = [
  "serde",
 ]
@@ -841,6 +841,7 @@ name = "cap"
 version = "0.1.0"
 dependencies = [
  "cap-camera",
+ "cap-displays",
  "cap-editor",
  "cap-export",
  "cap-flags",
@@ -852,7 +853,6 @@ dependencies = [
  "clap",
  "ffmpeg-next",
  "flume",
- "scap",
  "serde",
  "serde_json",
  "tokio",
@@ -865,8 +865,8 @@ dependencies = [
 name = "cap-audio"
 version = "0.1.0"
 dependencies = [
- "cidre",
- "cpal 0.15.3 (git+https://github.com/RustAudio/cpal?rev=f43d36e55494993bbbde3299af0c53e5cdf4d4cf)",
+ "cidre 0.11.0",
+ "cpal 0.16.0",
  "ffmpeg-next",
  "tokio",
 ]
@@ -877,7 +877,7 @@ version = "0.1.0"
 dependencies = [
  "cap-camera-avfoundation",
  "cap-camera-windows",
- "cidre",
+ "cidre 0.11.0",
  "inquire",
  "objc2-av-foundation",
  "serde",
@@ -891,7 +891,7 @@ dependencies = [
 name = "cap-camera-avfoundation"
 version = "0.1.0"
 dependencies = [
- "cidre",
+ "cidre 0.11.0",
  "clap",
  "inquire",
  "tracing",
@@ -916,7 +916,7 @@ dependencies = [
  "cap-camera",
  "cap-camera-avfoundation",
  "cap-camera-windows",
- "cidre",
+ "cidre 0.11.0",
  "ffmpeg-next",
  "inquire",
  "thiserror 1.0.69",
@@ -991,12 +991,12 @@ dependencies = [
  "cap-rendering",
  "cap-utils",
  "chrono",
- "cidre",
+ "cidre 0.11.0",
  "clipboard-rs",
  "cocoa 0.26.1",
  "core-foundation 0.10.1",
  "core-graphics 0.24.0",
- "cpal 0.15.3 (git+https://github.com/RustAudio/cpal?rev=f43d36e55494993bbbde3299af0c53e5cdf4d4cf)",
+ "cpal 0.16.0",
  "device_query",
  "dirs",
  "dotenvy_macro",
@@ -1065,6 +1065,7 @@ dependencies = [
 name = "cap-displays"
 version = "0.1.0"
 dependencies = [
+ "cidre 0.11.0",
  "cocoa 0.26.1",
  "core-foundation 0.10.1",
  "core-graphics 0.24.0",
@@ -1072,8 +1073,8 @@ dependencies = [
  "objc",
  "serde",
  "specta",
+ "tracing",
  "windows 0.60.0",
- "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -1086,10 +1087,11 @@ dependencies = [
  "cap-media-info",
  "cap-project",
  "cap-rendering",
- "cpal 0.15.3 (git+https://github.com/RustAudio/cpal?rev=f43d36e55494993bbbde3299af0c53e5cdf4d4cf)",
+ "cpal 0.16.0",
  "ffmpeg-next",
  "flume",
  "futures",
+ "ringbuf",
  "sentry",
  "serde",
  "specta",
@@ -1156,18 +1158,18 @@ dependencies = [
  "cap-camera",
  "cap-camera-ffmpeg",
  "cap-camera-windows",
+ "cap-displays",
  "cap-fail",
  "cap-flags",
  "cap-media-encoders",
  "cap-media-info",
  "cap-project",
- "cidre",
+ "cidre 0.11.0",
  "cocoa 0.26.1",
  "core-foundation 0.10.1",
  "core-graphics 0.24.0",
- "cpal 0.15.3 (git+https://github.com/RustAudio/cpal?rev=f43d36e55494993bbbde3299af0c53e5cdf4d4cf)",
+ "cpal 0.16.0",
  "ffmpeg-next",
- "ffmpeg-sys-next",
  "flume",
  "futures",
  "gif",
@@ -1181,9 +1183,13 @@ dependencies = [
  "objc2-foundation 0.2.2",
  "ringbuf",
  "scap",
+ "scap-direct3d",
+ "scap-ffmpeg",
+ "scap-screencapturekit",
  "screencapturekit",
  "serde",
  "specta",
+ "sync_wrapper",
  "tempfile",
  "thiserror 1.0.69",
  "tokio",
@@ -1203,7 +1209,7 @@ dependencies = [
  "cap-flags",
  "cap-media-info",
  "cap-project",
- "cidre",
+ "cidre 0.11.0",
  "ffmpeg-next",
  "gifski",
  "imgref",
@@ -1216,7 +1222,7 @@ dependencies = [
 name = "cap-media-info"
 version = "0.1.0"
 dependencies = [
- "cpal 0.15.3 (git+https://github.com/RustAudio/cpal?rev=f43d36e55494993bbbde3299af0c53e5cdf4d4cf)",
+ "cpal 0.16.0",
  "ffmpeg-next",
  "thiserror 1.0.69",
 ]
@@ -1239,9 +1245,13 @@ dependencies = [
 name = "cap-recording"
 version = "0.1.0"
 dependencies = [
+ "cap-audio",
+ "cap-camera",
+ "cap-camera-ffmpeg",
  "cap-cursor-capture",
  "cap-cursor-info",
  "cap-displays",
+ "cap-fail",
  "cap-flags",
  "cap-media",
  "cap-media-encoders",
@@ -1249,8 +1259,9 @@ dependencies = [
  "cap-project",
  "cap-utils",
  "chrono",
- "cidre",
+ "cidre 0.11.0",
  "cocoa 0.26.1",
+ "cpal 0.16.0",
  "device_query",
  "either",
  "ffmpeg-next",
@@ -1258,10 +1269,17 @@ dependencies = [
  "futures",
  "hex",
  "image 0.25.6",
+ "indexmap 2.10.0",
+ "inquire",
+ "kameo",
  "objc",
  "objc2-app-kit",
  "relative-path",
- "scap",
+ "ringbuf",
+ "scap-cpal",
+ "scap-direct3d",
+ "scap-ffmpeg",
+ "scap-screencapturekit",
  "screencapturekit",
  "serde",
  "serde_json",
@@ -1288,10 +1306,9 @@ dependencies = [
  "cap-flags",
  "cap-project",
  "cap-video-decode",
- "cidre",
+ "cidre 0.11.0",
  "ffmpeg-hw-device",
  "ffmpeg-next",
- "ffmpeg-sys-next",
  "futures",
  "futures-intrusive",
  "glyphon",
@@ -1344,10 +1361,9 @@ dependencies = [
 name = "cap-video-decode"
 version = "0.1.0"
 dependencies = [
- "cidre",
+ "cidre 0.11.0",
  "ffmpeg-hw-device",
  "ffmpeg-next",
- "ffmpeg-sys-next",
  "tokio",
  "tracing",
 ]
@@ -1382,7 +1398,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77"
 dependencies = [
  "serde",
- "toml 0.9.4",
+ "toml 0.9.5",
 ]
 
 [[package]]
@@ -1471,9 +1487,19 @@ dependencies = [
 [[package]]
 name = "cidre"
 version = "0.10.1"
-source = "git+https://github.com/CapSoftware/cidre?rev=517d097ae438#517d097ae4387ebb97f36c6e133b3c94dca78e25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7a105a9a8dd9e625e1e5938aa8442f63551990bfb2ab4bd606b1b30ccbfd8cf"
+dependencies = [
+ "cidre-macros 0.2.0",
+ "parking_lot",
+]
+
+[[package]]
+name = "cidre"
+version = "0.11.0"
+source = "git+https://github.com/CapSoftware/cidre?rev=bf84b67079a8#bf84b67079a89d6eaf01c1dab073baaba1ea8f77"
 dependencies = [
- "cidre-macros",
+ "cidre-macros 0.3.0",
  "parking_lot",
 ]
 
@@ -1483,6 +1509,12 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8f07a383a232853874a9b0b3e80e6eefe9bea73495b8ab4bdd960bc7c1db4c6d"
 
+[[package]]
+name = "cidre-macros"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82bc2f84c0baaa09299da3a03864491549685912c1e338a54211e00589dc1e4c"
+
 [[package]]
 name = "clang-sys"
 version = "1.8.1"
@@ -1913,6 +1945,20 @@ dependencies = [
  "coreaudio-sys",
 ]
 
+[[package]]
+name = "coreaudio-rs"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17"
+dependencies = [
+ "bitflags 1.3.2",
+ "libc",
+ "objc2-audio-toolbox",
+ "objc2-core-audio",
+ "objc2-core-audio-types",
+ "objc2-core-foundation",
+]
+
 [[package]]
 name = "coreaudio-sys"
 version = "0.2.17"
@@ -1953,7 +1999,7 @@ checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
 dependencies = [
  "alsa",
  "core-foundation-sys",
- "coreaudio-rs",
+ "coreaudio-rs 0.11.3",
  "dasp_sample",
  "jni",
  "js-sys",
@@ -1970,20 +2016,24 @@ dependencies = [
 
 [[package]]
 name = "cpal"
-version = "0.15.3"
-source = "git+https://github.com/RustAudio/cpal?rev=f43d36e55494993bbbde3299af0c53e5cdf4d4cf#f43d36e55494993bbbde3299af0c53e5cdf4d4cf"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f"
 dependencies = [
  "alsa",
- "core-foundation-sys",
- "coreaudio-rs",
+ "coreaudio-rs 0.13.0",
  "dasp_sample",
  "jni",
  "js-sys",
  "libc",
  "mach2",
- "ndk 0.8.0",
+ "ndk 0.9.0",
  "ndk-context",
- "oboe",
+ "num-derive",
+ "num-traits",
+ "objc2-audio-toolbox",
+ "objc2-core-audio",
+ "objc2-core-audio-types",
  "wasm-bindgen",
  "wasm-bindgen-futures",
  "web-sys",
@@ -2206,9 +2256,9 @@ dependencies = [
 
 [[package]]
 name = "derive_arbitrary"
-version = "1.4.1"
+version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
+checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2319,9 +2369,9 @@ dependencies = [
 
 [[package]]
 name = "dlopen2"
-version = "0.7.0"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6"
+checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff"
 dependencies = [
  "dlopen2_derive",
  "libc",
@@ -2439,7 +2489,7 @@ dependencies = [
  "cc",
  "memchr",
  "rustc_version",
- "toml 0.9.4",
+ "toml 0.9.5",
  "vswhom",
  "winreg",
 ]
@@ -2613,7 +2663,6 @@ name = "ffmpeg-hw-device"
 version = "0.1.0"
 dependencies = [
  "ffmpeg-next",
- "ffmpeg-sys-next",
 ]
 
 [[package]]
@@ -3463,9 +3512,9 @@ checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f"
 
 [[package]]
 name = "h2"
-version = "0.4.11"
+version = "0.4.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785"
+checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
 dependencies = [
  "atomic-waker",
  "bytes",
@@ -3724,7 +3773,7 @@ dependencies = [
  "js-sys",
  "log",
  "wasm-bindgen",
- "windows-core 0.61.2",
+ "windows-core 0.58.0",
 ]
 
 [[package]]
@@ -4386,7 +4435,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
 dependencies = [
  "cfg-if",
- "windows-targets 0.53.3",
+ "windows-targets 0.48.5",
 ]
 
 [[package]]
@@ -4587,15 +4636,6 @@ dependencies = [
  "syn 2.0.104",
 ]
 
-[[package]]
-name = "matchers"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
-dependencies = [
- "regex-automata 0.1.10",
-]
-
 [[package]]
 name = "matches"
 version = "0.1.10"
@@ -5146,6 +5186,21 @@ dependencies = [
  "objc2-quartz-core 0.3.1",
 ]
 
+[[package]]
+name = "objc2-audio-toolbox"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07"
+dependencies = [
+ "bitflags 2.9.1",
+ "libc",
+ "objc2 0.6.1",
+ "objc2-core-audio",
+ "objc2-core-audio-types",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.1",
+]
+
 [[package]]
 name = "objc2-av-foundation"
 version = "0.3.1"
@@ -5333,6 +5388,16 @@ dependencies = [
  "objc2-core-foundation",
 ]
 
+[[package]]
+name = "objc2-javascript-core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9052cb1bb50a4c161d934befcf879526fb87ae9a68858f241e693ca46225cf5a"
+dependencies = [
+ "objc2 0.6.1",
+ "objc2-core-foundation",
+]
+
 [[package]]
 name = "objc2-metal"
 version = "0.2.2"
@@ -5381,6 +5446,17 @@ dependencies = [
  "objc2-foundation 0.3.1",
 ]
 
+[[package]]
+name = "objc2-security"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1f8e0ef3ab66b08c42644dcb34dba6ec0a574bbd8adbb8bdbdc7a2779731a44"
+dependencies = [
+ "bitflags 2.9.1",
+ "objc2 0.6.1",
+ "objc2-core-foundation",
+]
+
 [[package]]
 name = "objc2-ui-kit"
 version = "0.3.1"
@@ -5405,6 +5481,8 @@ dependencies = [
  "objc2-app-kit",
  "objc2-core-foundation",
  "objc2-foundation 0.3.1",
+ "objc2-javascript-core",
+ "objc2-security",
 ]
 
 [[package]]
@@ -6519,17 +6597,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-automata 0.4.9",
- "regex-syntax 0.8.5",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
-dependencies = [
- "regex-syntax 0.6.29",
+ "regex-automata",
+ "regex-syntax",
 ]
 
 [[package]]
@@ -6540,15 +6609,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-syntax 0.8.5",
+ "regex-syntax",
 ]
 
-[[package]]
-name = "regex-syntax"
-version = "0.6.29"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
-
 [[package]]
 name = "regex-syntax"
 version = "0.8.5"
@@ -6714,7 +6777,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb"
 dependencies = [
  "claxon",
- "cpal 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cpal 0.15.3",
  "hound",
  "lewton",
  "symphonia",
@@ -6729,12 +6792,13 @@ checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
 
 [[package]]
 name = "rust-ini"
-version = "0.21.2"
+version = "0.21.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7295b7ce3bf4806b419dc3420745998b447178b7005e2011947b38fc5aa6791"
+checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f"
 dependencies = [
  "cfg-if",
  "ordered-multimap",
+ "trim-in-place",
 ]
 
 [[package]]
@@ -6886,10 +6950,10 @@ name = "scap"
 version = "0.1.0-beta.1"
 source = "git+https://github.com/CapSoftware/scap?rev=3cefe71561ff#3cefe71561ff735efe5c7bee69bcf32cf9ed3da7"
 dependencies = [
- "cidre",
+ "cidre 0.10.1",
  "cocoa 0.25.0",
  "core-graphics-helmer-fork",
- "cpal 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cpal 0.15.3",
  "dbus",
  "futures",
  "objc",
@@ -6901,10 +6965,21 @@ dependencies = [
  "windows-capture",
 ]
 
+[[package]]
+name = "scap-cpal"
+version = "0.1.0"
+dependencies = [
+ "cpal 0.16.0",
+ "thiserror 1.0.69",
+]
+
 [[package]]
 name = "scap-direct3d"
 version = "0.1.0"
 dependencies = [
+ "cap-displays",
+ "scap-ffmpeg",
+ "thiserror 1.0.69",
  "windows 0.60.0",
 ]
 
@@ -6912,11 +6987,15 @@ dependencies = [
 name = "scap-ffmpeg"
 version = "0.1.0"
 dependencies = [
- "cidre",
+ "cap-displays",
+ "cidre 0.11.0",
+ "cpal 0.16.0",
  "ffmpeg-next",
  "futures",
+ "scap-cpal",
  "scap-direct3d",
  "scap-screencapturekit",
+ "tokio",
  "windows 0.60.0",
 ]
 
@@ -6924,7 +7003,8 @@ dependencies = [
 name = "scap-screencapturekit"
 version = "0.1.0"
 dependencies = [
- "cidre",
+ "cap-displays",
+ "cidre 0.11.0",
  "clap",
  "futures",
  "inquire",
@@ -7217,9 +7297,9 @@ dependencies = [
 
 [[package]]
 name = "serde-untagged"
-version = "0.1.7"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "299d9c19d7d466db4ab10addd5703e4c615dec2a5a16dbbafe191045e87ee66e"
+checksum = "34836a629bcbc6f1afdf0907a744870039b1e14c0561cb26094fa683b158eff3"
 dependencies = [
  "erased-serde",
  "serde",
@@ -7345,9 +7425,9 @@ dependencies = [
 
 [[package]]
 name = "serialize-to-javascript"
-version = "0.1.1"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb"
+checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5"
 dependencies = [
  "serde",
  "serde_json",
@@ -7356,13 +7436,13 @@ dependencies = [
 
 [[package]]
 name = "serialize-to-javascript-impl"
-version = "0.1.1"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763"
+checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 2.0.104",
 ]
 
 [[package]]
@@ -7987,11 +8067,12 @@ dependencies = [
 
 [[package]]
 name = "tao"
-version = "0.34.0"
+version = "0.34.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49c380ca75a231b87b6c9dd86948f035012e7171d1a7c40a9c2890489a7ffd8a"
+checksum = "4daa814018fecdfb977b59a094df4bd43b42e8e21f88fddfc05807e6f46efaaf"
 dependencies = [
  "bitflags 2.9.1",
+ "block2 0.6.1",
  "core-foundation 0.10.1",
  "core-graphics 0.24.0",
  "crossbeam-channel",
@@ -8054,12 +8135,13 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
 
 [[package]]
 name = "tauri"
-version = "2.7.0"
+version = "2.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "352a4bc7bf6c25f5624227e3641adf475a6535707451b09bb83271df8b7a6ac7"
+checksum = "a54629607ea3084a8b455c1ebe888cbdfc4de02fa5edb2e40db0dc97091007e3"
 dependencies = [
  "anyhow",
  "bytes",
+ "cookie",
  "dirs",
  "dunce",
  "embed_plist",
@@ -8079,6 +8161,7 @@ dependencies = [
  "objc2-app-kit",
  "objc2-foundation 0.3.1",
  "objc2-ui-kit",
+ "objc2-web-kit",
  "percent-encoding",
  "plist",
  "raw-window-handle",
@@ -8107,9 +8190,9 @@ dependencies = [
 
 [[package]]
 name = "tauri-build"
-version = "2.3.1"
+version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "182d688496c06bf08ea896459bf483eb29cdff35c1c4c115fb14053514303064"
+checksum = "67945dbaf8920dbe3a1e56721a419a0c3d085254ab24cff5b9ad55e2b0016e0b"
 dependencies = [
  "anyhow",
  "cargo_toml",
@@ -8123,15 +8206,15 @@ dependencies = [
  "serde_json",
  "tauri-utils",
  "tauri-winres",
- "toml 0.8.23",
+ "toml 0.9.5",
  "walkdir",
 ]
 
 [[package]]
 name = "tauri-codegen"
-version = "2.3.1"
+version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b54a99a6cd8e01abcfa61508177e6096a4fe2681efecee9214e962f2f073ae4a"
+checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a"
 dependencies = [
  "base64 0.22.1",
  "brotli",
@@ -8156,9 +8239,9 @@ dependencies = [
 
 [[package]]
 name = "tauri-macros"
-version = "2.3.2"
+version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7945b14dc45e23532f2ded6e120170bbdd4af5ceaa45784a6b33d250fbce3f9e"
+checksum = "4368ea8094e7045217edb690f493b55b30caf9f3e61f79b4c24b6db91f07995e"
 dependencies = [
  "heck 0.5.0",
  "proc-macro2",
@@ -8186,9 +8269,9 @@ dependencies = [
 
 [[package]]
 name = "tauri-plugin"
-version = "2.3.1"
+version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5bd5c1e56990c70a906ef67a9851bbdba9136d26075ee9a2b19c8b46986b3e02"
+checksum = "9946a3cede302eac0c6eb6c6070ac47b1768e326092d32efbb91f21ed58d978f"
 dependencies = [
  "anyhow",
  "glob",
@@ -8197,7 +8280,7 @@ dependencies = [
  "serde",
  "serde_json",
  "tauri-utils",
- "toml 0.8.23",
+ "toml 0.9.5",
  "walkdir",
 ]
 
@@ -8238,9 +8321,9 @@ dependencies = [
 
 [[package]]
 name = "tauri-plugin-dialog"
-version = "2.3.2"
+version = "2.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37e5858cc7b455a73ab4ea2ebc08b5be33682c00ff1bf4cad5537d4fb62499d9"
+checksum = "0ee5a3c416dc59d7d9aa0de5490a82d6e201c67ffe97388979d77b69b08cda40"
 dependencies = [
  "log",
  "raw-window-handle",
@@ -8256,9 +8339,9 @@ dependencies = [
 
 [[package]]
 name = "tauri-plugin-fs"
-version = "2.4.1"
+version = "2.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c6ef84ee2f2094ce093e55106d90d763ba343fad57566992962e8f76d113f99"
+checksum = "315784ec4be45e90a987687bae7235e6be3d6e9e350d2b75c16b8a4bf22c1db7"
 dependencies = [
  "anyhow",
  "dunce",
@@ -8272,7 +8355,7 @@ dependencies = [
  "tauri-plugin",
  "tauri-utils",
  "thiserror 2.0.12",
- "toml 0.8.23",
+ "toml 0.9.5",
  "url",
 ]
 
@@ -8293,9 +8376,9 @@ dependencies = [
 
 [[package]]
 name = "tauri-plugin-http"
-version = "2.5.1"
+version = "2.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fcde333d97e565a7765aad82f32d8672458f7bd77b6ee653830d5dded9d7b5c2"
+checksum = "938a3d7051c9a82b431e3a0f3468f85715b3442b3c3a3913095e9fa509e2652c"
 dependencies = [
  "bytes",
  "cookie_store",
@@ -8350,9 +8433,9 @@ dependencies = [
 
 [[package]]
 name = "tauri-plugin-opener"
-version = "2.4.0"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecee219f11cdac713ab32959db5d0cceec4810ba4f4458da992292ecf9660321"
+checksum = "786156aa8e89e03d271fbd3fe642207da8e65f3c961baa9e2930f332bf80a1f5"
 dependencies = [
  "dunce",
  "glob",
@@ -8372,9 +8455,9 @@ dependencies = [
 
 [[package]]
 name = "tauri-plugin-os"
-version = "2.3.0"
+version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05bccb4c6de4299beec5a9b070878a01bce9e2c945aa7a75bcea38bcba4c675d"
+checksum = "77a1c77ebf6f20417ab2a74e8c310820ba52151406d0c80fbcea7df232e3f6ba"
 dependencies = [
  "gethostname 1.0.2",
  "log",
@@ -8452,9 +8535,9 @@ dependencies = [
 
 [[package]]
 name = "tauri-plugin-store"
-version = "2.3.0"
+version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5916c609664a56c82aeaefffca9851fd072d4d41f73d63f22ee3ee451508194f"
+checksum = "d85dd80d60a76ee2c2fdce09e9ef30877b239c2a6bb76e6d7d03708aa5f13a19"
 dependencies = [
  "dunce",
  "serde",
@@ -8515,9 +8598,9 @@ dependencies = [
 
 [[package]]
 name = "tauri-runtime"
-version = "2.7.1"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b1cc885be806ea15ff7b0eb47098a7b16323d9228876afda329e34e2d6c4676"
+checksum = "d4cfc9ad45b487d3fded5a4731a567872a4812e9552e3964161b08edabf93846"
 dependencies = [
  "cookie",
  "dpi",
@@ -8526,20 +8609,23 @@ dependencies = [
  "jni",
  "objc2 0.6.1",
  "objc2-ui-kit",
+ "objc2-web-kit",
  "raw-window-handle",
  "serde",
  "serde_json",
  "tauri-utils",
  "thiserror 2.0.12",
  "url",
+ "webkit2gtk",
+ "webview2-com",
  "windows 0.61.3",
 ]
 
 [[package]]
 name = "tauri-runtime-wry"
-version = "2.7.2"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe653a2fbbef19fe898efc774bc52c8742576342a33d3d028c189b57eb1d2439"
+checksum = "5bb0f10f831f75832ac74d14d98f701868f9a8adccef2c249b466cf70b607db9"
 dependencies = [
  "gtk",
  "http",
@@ -8592,9 +8678,9 @@ dependencies = [
 
 [[package]]
 name = "tauri-utils"
-version = "2.6.0"
+version = "2.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9330c15cabfe1d9f213478c9e8ec2b0c76dab26bb6f314b8ad1c8a568c1d186e"
+checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212"
 dependencies = [
  "anyhow",
  "brotli",
@@ -8621,7 +8707,7 @@ dependencies = [
  "serde_with",
  "swift-rs",
  "thiserror 2.0.12",
- "toml 0.8.23",
+ "toml 0.9.5",
  "url",
  "urlpattern",
  "uuid",
@@ -8630,13 +8716,12 @@ dependencies = [
 
 [[package]]
 name = "tauri-winres"
-version = "0.3.2"
+version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c6d9028d41d4de835e3c482c677a8cb88137ac435d6ff9a71f392d4421576c9"
+checksum = "fd21509dd1fa9bd355dc29894a6ff10635880732396aa38c0066c1e6c1ab8074"
 dependencies = [
  "embed-resource",
- "indexmap 2.10.0",
- "toml 0.9.4",
+ "toml 0.9.5",
 ]
 
 [[package]]
@@ -8937,9 +9022,9 @@ dependencies = [
 
 [[package]]
 name = "toml"
-version = "0.9.4"
+version = "0.9.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1"
+checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
 dependencies = [
  "indexmap 2.10.0",
  "serde",
@@ -9006,9 +9091,9 @@ dependencies = [
 
 [[package]]
 name = "toml_parser"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30"
+checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
 dependencies = [
  "winnow 0.7.12",
 ]
@@ -9133,14 +9218,10 @@ version = "0.3.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
 dependencies = [
- "matchers",
  "nu-ansi-term",
- "once_cell",
- "regex",
  "sharded-slab",
  "smallvec",
  "thread_local",
- "tracing",
  "tracing-core",
  "tracing-log",
 ]
@@ -9169,17 +9250,22 @@ dependencies = [
 
 [[package]]
 name = "tree_magic_mini"
-version = "3.1.6"
+version = "3.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aac5e8971f245c3389a5a76e648bfc80803ae066a1243a75db0064d7c1129d63"
+checksum = "f943391d896cdfe8eec03a04d7110332d445be7df856db382dd96a730667562c"
 dependencies = [
- "fnv",
  "memchr",
  "nom",
  "once_cell",
  "petgraph",
 ]
 
+[[package]]
+name = "trim-in-place"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc"
+
 [[package]]
 name = "try-lock"
 version = "0.2.5"
@@ -10060,7 +10146,7 @@ version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
 dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -10808,14 +10894,15 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
 
 [[package]]
 name = "wry"
-version = "0.52.1"
+version = "0.53.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12a714d9ba7075aae04a6e50229d6109e3d584774b99a6a8c60de1698ca111b9"
+checksum = "5698e50a589268aec06d2219f48b143222f7b5ad9aa690118b8dce0a8dcac574"
 dependencies = [
  "base64 0.22.1",
  "block2 0.6.1",
  "cookie",
  "crossbeam-channel",
+ "dirs",
  "dpi",
  "dunce",
  "gdkx11",
diff --git a/Cargo.toml b/Cargo.toml
index 3959865e21..d85f411f28 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,9 +6,9 @@ members = ["apps/cli", "apps/desktop/src-tauri", "crates/*"]
 anyhow = { version = "1.0.86" }
 # This includes a currently-unreleased fix that ensures the audio stream is actually
 # stopped and released on drop on macOS
-cpal = { git = "https://github.com/RustAudio/cpal", rev = "f43d36e55494993bbbde3299af0c53e5cdf4d4cf" }
+# cpal = { git = "https://github.com/RustAudio/cpal", rev = "f43d36e55494993bbbde3299af0c53e5cdf4d4cf" }
+cpal = "0.16"
 ffmpeg = { package = "ffmpeg-next", git = "https://github.com/CapSoftware/rust-ffmpeg", rev = "49db1fede112" }
-ffmpeg-sys-next = "7.1.0"
 tokio = { version = "1.39.3", features = [
 	"macros",
 	"process",
@@ -43,7 +43,7 @@ sentry = { version = "0.34.0", features = [
 tracing = "0.1.41"
 futures = "0.3.31"
 
-cidre = { git = "https://github.com/CapSoftware/cidre", rev = "517d097ae438", features = [
+cidre = { git = "https://github.com/CapSoftware/cidre", rev = "bf84b67079a8", features = [
 	"macos_13_0",
 	"cv",
 	"cf",
@@ -79,6 +79,6 @@ debug = true
 
 [patch.crates-io]
 screencapturekit = { git = "https://github.com/CapSoftware/screencapturekit-rs", rev = "7ff1e103742e56c8f6c2e940b5e52684ed0bed69" } # branch = "cap-main"
-cidre = { git = "https://github.com/CapSoftware/cidre", rev = "517d097ae438" }
+cidre = { git = "https://github.com/CapSoftware/cidre", rev = "bf84b67079a8" }
 # https://github.com/gfx-rs/wgpu/pull/7550
 # wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "cd41a6e32a6239b65d1cecbeccde6a43a100914a" }
diff --git a/LICENSE b/LICENSE
index ce1326157c..1e91201891 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,5 +1,11 @@
 Copyright (c) 2023-present Cap Software, Inc.
-          
+
+Portions of this software are licensed as follows:
+
+- All code residing in the `cap-camera*` and `scap-*` families of crates is licensed under the MIT License (see LICENSE-MIT).
+- All third party components are licensed under the original license provided by the owner of the applicable component
+- All other content not mentioned above is available under the AGPLv3 license as defined below
+
                     GNU AFFERO GENERAL PUBLIC LICENSE
                        Version 3, 19 November 2007
 
diff --git a/LICENSE-MIT b/LICENSE-MIT
new file mode 100644
index 0000000000..53982646c3
--- /dev/null
+++ b/LICENSE-MIT
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Cap Software, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/apps/cli/Cargo.toml b/apps/cli/Cargo.toml
index 05105bf0d7..6a9a6aeac0 100644
--- a/apps/cli/Cargo.toml
+++ b/apps/cli/Cargo.toml
@@ -14,10 +14,10 @@ cap-flags = { path = "../../crates/flags" }
 cap-recording = { path = "../../crates/recording" }
 cap-export = { path = "../../crates/export" }
 cap-camera = { path = "../../crates/camera" }
+cap-displays = { path = "../../crates/displays" }
 serde = { workspace = true }
 serde_json = "1.0.133"
 tokio.workspace = true
-scap.workspace = true
 uuid = { version = "1.11.1", features = ["v4"] }
 ffmpeg = { workspace = true }
 tracing.workspace = true
diff --git a/apps/cli/src/main.rs b/apps/cli/src/main.rs
index 87285f4d96..00f7a959c8 100644
--- a/apps/cli/src/main.rs
+++ b/apps/cli/src/main.rs
@@ -6,8 +6,8 @@ use std::{
 };
 
 use cap_export::ExporterBase;
-use cap_media::{feeds::CameraFeed, sources::get_target_fps};
 use cap_project::XY;
+use cap_recording::feeds::CameraFeed;
 use clap::{Args, Parser, Subcommand};
 use record::RecordStart;
 use serde_json::json;
@@ -77,7 +77,7 @@ async fn main() -> Result<(), String> {
         }
         Commands::Record(RecordArgs { command, args }) => match command {
             Some(RecordCommands::Screens) => {
-                let screens = cap_media::sources::list_screens();
+                let screens = cap_recording::screen_capture::list_displays();
 
                 for (i, (screen, target)) in screens.iter().enumerate() {
                     println!(
@@ -89,12 +89,12 @@ screen {}:
                         i,
                         screen.id,
                         screen.name,
-                        get_target_fps(target).unwrap()
+                        target.refresh_rate()
                     );
                 }
             }
             Some(RecordCommands::Windows) => {
-                let windows = cap_media::sources::list_windows();
+                let windows = cap_recording::screen_capture::list_windows();
 
                 for (i, (window, target)) in windows.iter().enumerate() {
                     println!(
@@ -106,7 +106,7 @@ window {}:
                         i,
                         window.id,
                         window.name,
-                        get_target_fps(target).unwrap()
+                        target.display().unwrap().refresh_rate()
                     );
                 }
             }
diff --git a/apps/cli/src/record.rs b/apps/cli/src/record.rs
index 92d9b8bb11..b8f3952aea 100644
--- a/apps/cli/src/record.rs
+++ b/apps/cli/src/record.rs
@@ -1,8 +1,8 @@
-use std::{env::current_dir, path::PathBuf, sync::Arc};
-
 use cap_camera::ModelID;
-use cap_media::sources::ScreenCaptureTarget;
+use cap_displays::{DisplayId, WindowId};
+use cap_recording::screen_capture::ScreenCaptureTarget;
 use clap::Args;
+use std::{env::current_dir, path::PathBuf, sync::Arc};
 use tokio::{io::AsyncBufReadExt, sync::Mutex};
 use uuid::Uuid;
 
@@ -29,16 +29,16 @@ pub struct RecordStart {
 
 impl RecordStart {
     pub async fn run(self) -> Result<(), String> {
-        let (target_info, _) = match (self.target.screen, self.target.window) {
-            (Some(id), _) => cap_media::sources::list_screens()
+        let target_info = match (self.target.screen, self.target.window) {
+            (Some(id), _) => cap_recording::screen_capture::list_displays()
                 .into_iter()
                 .find(|s| s.0.id == id)
-                .map(|(s, t)| (ScreenCaptureTarget::Screen { id: s.id }, t))
+                .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id })
                 .ok_or(format!("Screen with id '{id}' not found")),
-            (_, Some(id)) => cap_media::sources::list_windows()
+            (_, Some(id)) => cap_recording::screen_capture::list_windows()
                 .into_iter()
                 .find(|s| s.0.id == id)
-                .map(|(s, t)| (ScreenCaptureTarget::Window { id: s.id }, t))
+                .map(|(s, _)| ScreenCaptureTarget::Window { id: s.id })
                 .ok_or(format!("Window with id '{id}' not found")),
             _ => Err("No target specified".to_string()),
         }?;
@@ -90,8 +90,8 @@ impl RecordStart {
 struct RecordTargets {
     /// ID of the screen to capture
     #[arg(long, group = "target")]
-    screen: Option<u32>,
+    screen: Option<DisplayId>,
     /// ID of the window to capture
     #[arg(long, group = "target")]
-    window: Option<u32>,
+    window: Option<WindowId>,
 }
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index 126dd75d4b..f7d299f51f 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -45,11 +45,11 @@
 		"@tauri-apps/plugin-fs": "^2.4.1",
 		"@tauri-apps/plugin-http": "^2.5.1",
 		"@tauri-apps/plugin-notification": "^2.3.0",
-		"@tauri-apps/plugin-opener": "^2.4.0",
+		"@tauri-apps/plugin-opener": "^2.5.0",
 		"@tauri-apps/plugin-os": "^2.3.0",
 		"@tauri-apps/plugin-process": "2.3.0",
 		"@tauri-apps/plugin-shell": "^2.3.0",
-		"@tauri-apps/plugin-store": "^2.3.0",
+		"@tauri-apps/plugin-store": "^2.4.0",
 		"@tauri-apps/plugin-updater": "^2.9.0",
 		"@ts-rest/core": "^3.52.1",
 		"@types/react-tooltip": "^4.2.4",
diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml
index af38d91fb8..4d13c21bf4 100644
--- a/apps/desktop/src-tauri/Cargo.toml
+++ b/apps/desktop/src-tauri/Cargo.toml
@@ -45,13 +45,13 @@ tauri-plugin-deep-link = "2.2.0"
 tauri-plugin-clipboard-manager = "2.2.1"
 tauri-plugin-opener = "2.2.6"
 
+scap = { workspace = true }
 serde = { workspace = true }
 serde_json = "1.0.111"
 specta.workspace = true
 specta-typescript = "0.0.7"
 tokio.workspace = true
 uuid = { version = "1.10.0", features = ["v4"] }
-scap.workspace = true
 image = "0.25.2"
 mp4 = "0.14.0"
 futures-intrusive = "0.5.0"
diff --git a/apps/desktop/src-tauri/src/audio_meter.rs b/apps/desktop/src-tauri/src/audio_meter.rs
index 8d69bf0204..b5bd335a7b 100644
--- a/apps/desktop/src-tauri/src/audio_meter.rs
+++ b/apps/desktop/src-tauri/src/audio_meter.rs
@@ -1,4 +1,4 @@
-use cap_media::feeds::{AudioInputSamples, AudioInputSamplesReceiver};
+use cap_recording::feeds::{AudioInputSamples, AudioInputSamplesReceiver};
 use cpal::{SampleFormat, StreamInstant};
 use keyed_priority_queue::KeyedPriorityQueue;
 use serde::{Deserialize, Serialize};
diff --git a/apps/desktop/src-tauri/src/camera.rs b/apps/desktop/src-tauri/src/camera.rs
index e587fa90f9..3013444078 100644
--- a/apps/desktop/src-tauri/src/camera.rs
+++ b/apps/desktop/src-tauri/src/camera.rs
@@ -1,5 +1,5 @@
 use anyhow::Context;
-use cap_media::feeds::RawCameraFrame;
+use cap_recording::feeds::RawCameraFrame;
 use ffmpeg::{
     format::{self, Pixel},
     frame,
diff --git a/apps/desktop/src-tauri/src/camera_legacy.rs b/apps/desktop/src-tauri/src/camera_legacy.rs
index 6b5ad7c958..bb1afa70d5 100644
--- a/apps/desktop/src-tauri/src/camera_legacy.rs
+++ b/apps/desktop/src-tauri/src/camera_legacy.rs
@@ -1,7 +1,9 @@
-use cap_media::{feeds::RawCameraFrame, frame_ws::WSFrame};
+use cap_recording::feeds::RawCameraFrame;
 use flume::Sender;
 use tokio_util::sync::CancellationToken;
 
+use crate::frame_ws::{WSFrame, create_frame_ws};
+
 pub async fn create_camera_preview_ws() -> (Sender<RawCameraFrame>, u16, CancellationToken) {
     let (camera_tx, mut _camera_rx) = flume::bounded::<RawCameraFrame>(4);
     let (_camera_tx, camera_rx) = flume::bounded::<WSFrame>(4);
@@ -64,7 +66,7 @@ pub async fn create_camera_preview_ws() -> (Sender<RawCameraFrame>, u16, Cancell
         }
     });
     // _shutdown needs to be kept alive to keep the camera ws running
-    let (camera_ws_port, _shutdown) = cap_media::frame_ws::create_frame_ws(camera_rx.clone()).await;
+    let (camera_ws_port, _shutdown) = create_frame_ws(camera_rx.clone()).await;
 
     (camera_tx, camera_ws_port, _shutdown)
 }
diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs
index c71e4d1c27..c6e2f9296b 100644
--- a/apps/desktop/src-tauri/src/deeplink_actions.rs
+++ b/apps/desktop/src-tauri/src/deeplink_actions.rs
@@ -1,7 +1,6 @@
-use std::path::{Path, PathBuf};
-
-use cap_recording::RecordingMode;
+use cap_recording::{RecordingMode, feeds::DeviceOrModelID, sources::ScreenCaptureTarget};
 use serde::{Deserialize, Serialize};
+use std::path::{Path, PathBuf};
 use tauri::{AppHandle, Manager, Url};
 use tracing::trace;
 
@@ -21,7 +20,7 @@ pub enum CaptureMode {
 pub enum DeepLinkAction {
     StartRecording {
         capture_mode: CaptureMode,
-        camera: Option<cap_media::feeds::DeviceOrModelID>,
+        camera: Option<DeviceOrModelID>,
         mic_label: Option<String>,
         capture_system_audio: bool,
         mode: RecordingMode,
@@ -121,14 +120,13 @@ impl DeepLinkAction {
                 crate::set_camera_input(app.clone(), state.clone(), camera_preview, camera).await?;
                 crate::set_mic_input(state.clone(), mic_label).await?;
 
-                use cap_media::sources::ScreenCaptureTarget;
                 let capture_target: ScreenCaptureTarget = match capture_mode {
-                    CaptureMode::Screen(name) => cap_media::sources::list_screens()
+                    CaptureMode::Screen(name) => cap_recording::screen_capture::list_displays()
                         .into_iter()
                         .find(|(s, _)| s.name == name)
-                        .map(|(s, _)| ScreenCaptureTarget::Screen { id: s.id })
+                        .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id })
                         .ok_or(format!("No screen with name \"{}\"", &name))?,
-                    CaptureMode::Window(name) => cap_media::sources::list_windows()
+                    CaptureMode::Window(name) => cap_recording::screen_capture::list_windows()
                         .into_iter()
                         .find(|(w, _)| w.name == name)
                         .map(|(w, _)| ScreenCaptureTarget::Window { id: w.id })
diff --git a/apps/desktop/src-tauri/src/editor_window.rs b/apps/desktop/src-tauri/src/editor_window.rs
index d75f59276e..7c253d595a 100644
--- a/apps/desktop/src-tauri/src/editor_window.rs
+++ b/apps/desktop/src-tauri/src/editor_window.rs
@@ -1,10 +1,34 @@
 use std::{collections::HashMap, ops::Deref, path::PathBuf, sync::Arc};
-
-use cap_editor::EditorInstance;
 use tauri::{Manager, Runtime, Window, ipc::CommandArg};
 use tokio::sync::RwLock;
+use tokio_util::sync::CancellationToken;
+
+use crate::{
+    create_editor_instance_impl,
+    frame_ws::{WSFrame, create_frame_ws},
+};
+
+pub struct EditorInstance {
+    inner: Arc<cap_editor::EditorInstance>,
+    pub ws_port: u16,
+    pub ws_shutdown_token: CancellationToken,
+}
+
+impl EditorInstance {
+    pub async fn dispose(&self) {
+        self.inner.dispose().await;
+
+        self.ws_shutdown_token.cancel();
+    }
+}
 
-use crate::create_editor_instance_impl;
+impl Deref for EditorInstance {
+    type Target = Arc<cap_editor::EditorInstance>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.inner
+    }
+}
 
 #[derive(Clone)]
 pub struct EditorInstances(Arc<RwLock<HashMap<String, Arc<EditorInstance>>>>);
@@ -64,8 +88,31 @@ impl EditorInstances {
 
         match instances.entry(window.label().to_string()) {
             Entry::Vacant(entry) => {
-                let instance = create_editor_instance_impl(window.app_handle(), path).await?;
+                let (frame_tx, frame_rx) = flume::bounded(4);
+
+                let (ws_port, ws_shutdown_token) = create_frame_ws(frame_rx).await;
+                let instance = create_editor_instance_impl(
+                    window.app_handle(),
+                    path,
+                    Box::new(move |frame| {
+                        let _ = frame_tx.send(WSFrame {
+                            data: frame.data,
+                            width: frame.width,
+                            height: frame.height,
+                            stride: frame.padded_bytes_per_row,
+                        });
+                    }),
+                )
+                .await?;
+
+                let instance = Arc::new(EditorInstance {
+                    inner: instance.clone(),
+                    ws_port,
+                    ws_shutdown_token,
+                });
+
                 entry.insert(instance.clone());
+
                 Ok(instance)
             }
             Entry::Occupied(entry) => Ok(entry.get().clone()),
diff --git a/apps/desktop/src-tauri/src/fake_window.rs b/apps/desktop/src-tauri/src/fake_window.rs
index a1b5b2bc6c..ce273e45b4 100644
--- a/apps/desktop/src-tauri/src/fake_window.rs
+++ b/apps/desktop/src-tauri/src/fake_window.rs
@@ -1,16 +1,16 @@
-use cap_media::platform::Bounds;
+use cap_displays::bounds::LogicalBounds;
 use std::{collections::HashMap, sync::Arc, time::Duration};
 use tauri::{AppHandle, Manager, WebviewWindow};
 use tokio::{sync::RwLock, time::sleep};
 
-pub struct FakeWindowBounds(pub Arc<RwLock<HashMap<String, HashMap<String, Bounds>>>>);
+pub struct FakeWindowBounds(pub Arc<RwLock<HashMap<String, HashMap<String, LogicalBounds>>>>);
 
 #[tauri::command]
 #[specta::specta]
 pub async fn set_fake_window_bounds(
     window: tauri::Window,
     name: String,
-    bounds: Bounds,
+    bounds: LogicalBounds,
     state: tauri::State<'_, FakeWindowBounds>,
 ) -> Result<(), String> {
     let mut state = state.0.write().await;
@@ -70,10 +70,12 @@ pub fn spawn_fake_window_listener(app: AppHandle, window: WebviewWindow) {
             let mut ignore = true;
 
             for bounds in windows.values() {
-                let x_min = (window_position.x as f64) + bounds.x * scale_factor;
-                let x_max = (window_position.x as f64) + (bounds.x + bounds.width) * scale_factor;
-                let y_min = (window_position.y as f64) + bounds.y * scale_factor;
-                let y_max = (window_position.y as f64) + (bounds.y + bounds.height) * scale_factor;
+                let x_min = (window_position.x as f64) + bounds.position().x() * scale_factor;
+                let x_max = (window_position.x as f64)
+                    + (bounds.position().x() + bounds.size().width()) * scale_factor;
+                let y_min = (window_position.y as f64) + bounds.position().y() * scale_factor;
+                let y_max = (window_position.y as f64)
+                    + (bounds.position().y() + bounds.size().height()) * scale_factor;
 
                 if mouse_position.x >= x_min
                     && mouse_position.x <= x_max
diff --git a/crates/media/src/frame_ws.rs b/apps/desktop/src-tauri/src/frame_ws.rs
similarity index 100%
rename from crates/media/src/frame_ws.rs
rename to apps/desktop/src-tauri/src/frame_ws.rs
diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs
index bd1e570651..2a68041111 100644
--- a/apps/desktop/src-tauri/src/lib.rs
+++ b/apps/desktop/src-tauri/src/lib.rs
@@ -9,6 +9,7 @@ mod editor_window;
 mod export;
 mod fake_window;
 mod flags;
+mod frame_ws;
 mod general_settings;
 mod hotkeys;
 mod notifications;
@@ -25,18 +26,23 @@ mod windows;
 use audio::AppSounds;
 use auth::{AuthStore, AuthenticationInvalid, Plan};
 use camera::{CameraPreview, CameraWindowState};
+use cap_displays::{DisplayId, WindowId, bounds::LogicalBounds};
 use cap_editor::EditorInstance;
 use cap_editor::EditorState;
-use cap_media::feeds::RawCameraFrame;
-use cap_media::feeds::{AudioInputFeed, AudioInputSamplesSender};
-use cap_media::platform::Bounds;
-use cap_media::{feeds::CameraFeed, sources::ScreenCaptureTarget};
 use cap_project::RecordingMetaInner;
 use cap_project::XY;
 use cap_project::{
     ProjectConfiguration, RecordingMeta, SharingMeta, StudioRecordingMeta, ZoomSegment,
 };
+use cap_recording::feeds::DeviceOrModelID;
+use cap_recording::{
+    feeds::CameraFeed,
+    feeds::RawCameraFrame,
+    feeds::{AudioInputFeed, AudioInputSamplesSender},
+    sources::ScreenCaptureTarget,
+};
 use cap_rendering::ProjectRecordingsMeta;
+use cap_rendering::RenderedFrame;
 use clipboard_rs::common::RustImage;
 use clipboard_rs::{Clipboard, ClipboardContext};
 use editor_window::EditorInstances;
@@ -250,7 +256,7 @@ async fn set_camera_input(
     app_handle: AppHandle,
     state: MutableState<'_, App>,
     camera_preview: State<'_, CameraPreview>,
-    id: Option<cap_media::feeds::DeviceOrModelID>,
+    id: Option<DeviceOrModelID>,
 ) -> Result<bool, String> {
     let mut app = state.write().await;
 
@@ -433,9 +439,17 @@ pub struct RecordingInfo {
 #[derive(Serialize, Type)]
 #[serde(rename_all = "camelCase")]
 enum CurrentRecordingTarget {
-    Window { id: u32, bounds: Bounds },
-    Screen { id: u32 },
-    Area { screen: u32, bounds: Bounds },
+    Window {
+        id: WindowId,
+        bounds: LogicalBounds,
+    },
+    Screen {
+        id: DisplayId,
+    },
+    Area {
+        screen: DisplayId,
+        bounds: LogicalBounds,
+    },
 }
 
 #[derive(Serialize, Type)]
@@ -451,29 +465,37 @@ async fn get_current_recording(
     state: MutableState<'_, App>,
 ) -> Result<JsonValue<Option<CurrentRecording>>, ()> {
     let state = state.read().await;
-    Ok(JsonValue::new(&state.current_recording().map(|r| {
-        let bounds = r.bounds();
-
-        let target = match r.capture_target() {
-            ScreenCaptureTarget::Screen { id } => CurrentRecordingTarget::Screen { id: *id },
-            ScreenCaptureTarget::Window { id } => CurrentRecordingTarget::Window {
-                id: *id,
-                bounds: *bounds,
-            },
-            ScreenCaptureTarget::Area { screen, bounds } => CurrentRecordingTarget::Area {
-                screen: *screen,
-                bounds: *bounds,
-            },
-        };
+    Ok(JsonValue::new(
+        &state
+            .current_recording()
+            .map(|r| {
+                let target = match r.capture_target() {
+                    ScreenCaptureTarget::Display { id } => {
+                        CurrentRecordingTarget::Screen { id: id.clone() }
+                    }
+                    ScreenCaptureTarget::Window { id } => CurrentRecordingTarget::Window {
+                        id: id.clone(),
+                        bounds: cap_displays::Window::from_id(id)
+                            .ok_or(())?
+                            .display_relative_logical_bounds()
+                            .ok_or(())?,
+                    },
+                    ScreenCaptureTarget::Area { screen, bounds } => CurrentRecordingTarget::Area {
+                        screen: screen.clone(),
+                        bounds: bounds.clone(),
+                    },
+                };
 
-        CurrentRecording {
-            target,
-            r#type: match r {
-                InProgressRecording::Instant { .. } => RecordingType::Instant,
-                InProgressRecording::Studio { .. } => RecordingType::Studio,
-            },
-        }
-    })))
+                Ok(CurrentRecording {
+                    target,
+                    r#type: match r {
+                        InProgressRecording::Instant { .. } => RecordingType::Instant,
+                        InProgressRecording::Studio { .. } => RecordingType::Studio,
+                    },
+                })
+            })
+            .transpose()?,
+    ))
 }
 
 #[derive(Serialize, Type, tauri_specta::Event, Clone)]
@@ -1896,7 +1918,7 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
             recording::delete_recording,
             recording::list_cameras,
             recording::list_capture_windows,
-            recording::list_capture_screens,
+            recording::list_capture_displays,
             take_screenshot,
             list_audio_devices,
             close_recordings_overlay_window,
@@ -1992,13 +2014,13 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
         .typ::<general_settings::GeneralSettingsStore>()
         .typ::<cap_flags::Flags>();
 
-    // #[cfg(debug_assertions)]
-    // specta_builder
-    //     .export(
-    //         specta_typescript::Typescript::default(),
-    //         "../src/utils/tauri.ts",
-    //     )
-    //     .expect("Failed to export typescript bindings");
+    #[cfg(debug_assertions)]
+    specta_builder
+        .export(
+            specta_typescript::Typescript::default(),
+            "../src/utils/tauri.ts",
+        )
+        .expect("Failed to export typescript bindings");
 
     let (camera_tx, camera_ws_port, _shutdown) = camera_legacy::create_camera_preview_ws().await;
 
@@ -2306,16 +2328,21 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
 async fn create_editor_instance_impl(
     app: &AppHandle,
     path: PathBuf,
+    frame_cb: Box<dyn FnMut(RenderedFrame) + Send>,
 ) -> Result<Arc<EditorInstance>, String> {
     let app = app.clone();
 
-    let instance = EditorInstance::new(path, {
+    let instance = {
         let app = app.clone();
-        move |state| {
-            EditorStateChanged::new(state).emit(&app).ok();
-        }
-    })
-    .await?;
+        EditorInstance::new(
+            path,
+            move |state| {
+                let _ = EditorStateChanged::new(state).emit(&app);
+            },
+            frame_cb,
+        )
+        .await?
+    };
 
     RenderFrameEvent::listen_any(&app, {
         let preview_tx = instance.preview_tx.clone();
diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs
index 01e90208b8..918d156bd1 100644
--- a/apps/desktop/src-tauri/src/recording.rs
+++ b/apps/desktop/src-tauri/src/recording.rs
@@ -1,27 +1,4 @@
-use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Duration};
-
-use crate::{
-    App, CurrentRecordingChanged, MutableState, NewStudioRecordingAdded, RecordingStarted,
-    RecordingStopped, VideoUploadInfo,
-    audio::AppSounds,
-    auth::AuthStore,
-    create_screenshot,
-    general_settings::{GeneralSettingsStore, PostDeletionBehaviour, PostStudioRecordingBehaviour},
-    open_external_link,
-    presets::PresetsStore,
-    upload::{
-        InstantMultipartUpload, build_video_meta, create_or_get_video, prepare_screenshot_upload,
-        upload_video,
-    },
-    web_api::ManagerExt,
-    windows::{CapWindowId, ShowCapWindow},
-};
 use cap_fail::fail;
-use cap_media::{feeds::CameraFeed, platform::display_for_window, sources::ScreenCaptureTarget};
-use cap_media::{
-    platform::Bounds,
-    sources::{CaptureScreen, CaptureWindow},
-};
 use cap_project::{
     CursorClickEvent, Platform, ProjectConfiguration, RecordingMeta, RecordingMetaInner,
     SharingMeta, StudioRecordingMeta, TimelineConfiguration, TimelineSegment, ZoomMode,
@@ -29,17 +6,37 @@ use cap_project::{
 };
 use cap_recording::{
     CompletedStudioRecording, RecordingError, RecordingMode, StudioRecordingHandle,
+    feeds::CameraFeed,
     instant_recording::{CompletedInstantRecording, InstantRecordingHandle},
+    sources::{CaptureDisplay, CaptureWindow, ScreenCaptureTarget, screen_capture},
 };
 use cap_rendering::ProjectRecordingsMeta;
 use cap_utils::{ensure_dir, spawn_actor};
 use serde::Deserialize;
 use specta::Type;
+use std::{path::PathBuf, sync::Arc, time::Duration};
 use tauri::{AppHandle, Manager};
 use tauri_plugin_dialog::{DialogExt, MessageDialogBuilder};
 use tauri_specta::Event;
 use tracing::{error, info};
 
+use crate::{
+    App, CurrentRecordingChanged, MutableState, NewStudioRecordingAdded, RecordingStarted,
+    RecordingStopped, VideoUploadInfo,
+    audio::AppSounds,
+    auth::AuthStore,
+    create_screenshot,
+    general_settings::{GeneralSettingsStore, PostDeletionBehaviour, PostStudioRecordingBehaviour},
+    open_external_link,
+    presets::PresetsStore,
+    upload::{
+        InstantMultipartUpload, build_video_meta, create_or_get_video, prepare_screenshot_upload,
+        upload_video,
+    },
+    web_api::ManagerExt,
+    windows::{CapWindowId, ShowCapWindow},
+};
+
 pub enum InProgressRecording {
     Instant {
         target_name: String,
@@ -79,10 +76,10 @@ impl InProgressRecording {
         }
     }
 
-    pub async fn resume(&self) -> Result<(), RecordingError> {
+    pub async fn resume(&self) -> Result<(), String> {
         match self {
-            Self::Instant { handle, .. } => handle.resume().await,
-            Self::Studio { handle, .. } => handle.resume().await,
+            Self::Instant { handle, .. } => handle.resume().await.map_err(|e| e.to_string()),
+            Self::Studio { handle, .. } => handle.resume().await.map_err(|e| e.to_string()),
         }
     }
 
@@ -124,13 +121,6 @@ impl InProgressRecording {
             Self::Studio { handle, .. } => handle.cancel().await,
         }
     }
-
-    pub fn bounds(&self) -> &Bounds {
-        match self {
-            Self::Instant { handle, .. } => &handle.bounds,
-            Self::Studio { handle, .. } => &handle.bounds,
-        }
-    }
 }
 
 pub enum CompletedRecording {
@@ -171,8 +161,8 @@ impl CompletedRecording {
 
 #[tauri::command(async)]
 #[specta::specta]
-pub async fn list_capture_screens() -> Vec<CaptureScreen> {
-    cap_media::sources::list_screens()
+pub async fn list_capture_displays() -> Vec<CaptureDisplay> {
+    screen_capture::list_displays()
         .into_iter()
         .map(|(v, _)| v)
         .collect()
@@ -181,7 +171,7 @@ pub async fn list_capture_screens() -> Vec<CaptureScreen> {
 #[tauri::command(async)]
 #[specta::specta]
 pub async fn list_capture_windows() -> Vec<CaptureWindow> {
-    cap_media::sources::list_windows()
+    screen_capture::list_windows()
         .into_iter()
         .map(|(v, _)| v)
         .collect()
@@ -237,23 +227,12 @@ pub async fn start_recording(
         .await?;
 
     let target_name = {
-        let title = inputs.capture_target.get_title();
-
-        match inputs.capture_target {
-            ScreenCaptureTarget::Area { .. } => "Area".to_string(),
-            ScreenCaptureTarget::Window { id, .. } => {
-                let platform_windows: HashMap<u32, cap_media::platform::Window> =
-                    cap_media::platform::get_on_screen_windows()
-                        .into_iter()
-                        .map(|window| (window.window_id, window))
-                        .collect();
-
-                platform_windows
-                    .get(&id)
-                    .map(|v| v.owner_name.to_string())
-                    .unwrap_or_else(|| "Window".to_string())
-            }
-            ScreenCaptureTarget::Screen { .. } => title.unwrap_or_else(|| "Screen".to_string()),
+        let title = inputs.capture_target.title();
+
+        match inputs.capture_target.clone() {
+            ScreenCaptureTarget::Area { .. } => title.unwrap_or_else(|| "Area".to_string()),
+            ScreenCaptureTarget::Window { .. } => title.unwrap_or_else(|| "Window".to_string()),
+            ScreenCaptureTarget::Display { .. } => title.unwrap_or_else(|| "Screen".to_string()),
         }
     };
 
@@ -302,28 +281,20 @@ pub async fn start_recording(
 
     match &inputs.capture_target {
         ScreenCaptureTarget::Window { id: _id } => {
-            #[cfg(target_os = "macos")]
-            let display = display_for_window(*_id).unwrap().id;
-
-            #[cfg(windows)]
-            let display = {
-                let scap::Target::Window(target) = inputs.capture_target.get_target().unwrap()
-                else {
-                    unreachable!();
-                };
-                display_for_window(windows::Win32::Foundation::HWND(target.raw_handle.0))
-                    .unwrap()
-                    .0 as u32
-            };
-
-            let _ = ShowCapWindow::WindowCaptureOccluder { screen_id: display }
-                .show(&app)
-                .await;
+            if let Some(show) = inputs
+                .capture_target
+                .display()
+                .map(|d| ShowCapWindow::WindowCaptureOccluder { screen_id: d.id() })
+            {
+                let _ = show.show(&app).await;
+            }
         }
         ScreenCaptureTarget::Area { screen, .. } => {
-            let _ = ShowCapWindow::WindowCaptureOccluder { screen_id: *screen }
-                .show(&app)
-                .await;
+            let _ = ShowCapWindow::WindowCaptureOccluder {
+                screen_id: screen.clone(),
+            }
+            .show(&app)
+            .await;
         }
         _ => {}
     }
@@ -334,13 +305,6 @@ pub async fn start_recording(
         state.set_pending_recording();
     }
 
-    if let Some(window) = CapWindowId::Main.get(&app) {
-        let _ = general_settings
-            .map(|v| v.main_window_recording_start_behaviour)
-            .unwrap_or_default()
-            .perform(&window);
-    }
-
     let countdown = general_settings.and_then(|v| v.recording_countdown);
     let _ = ShowCapWindow::InProgressRecording { countdown }
         .show(&app)
@@ -372,57 +336,39 @@ pub async fn start_recording(
 
     println!("spawning actor");
 
-    // done in spawn to catch panics just in case
-    let actor_done_rx = spawn_actor({
-        let state_mtx = Arc::clone(&state_mtx);
-        let general_settings = general_settings.cloned();
-        async move {
-            fail!("recording::spawn_actor");
-            let mut state = state_mtx.write().await;
-
-            let base_inputs = cap_recording::RecordingBaseInputs {
-                capture_target: inputs.capture_target,
-                capture_system_audio: inputs.capture_system_audio,
-                mic_feed: &state.mic_feed,
-            };
+    if let Some(window) = CapWindowId::Main.get(&app) {
+        let _ = general_settings
+            .map(|v| v.main_window_recording_start_behaviour)
+            .unwrap_or_default()
+            .perform(&window);
+    }
 
-            let (actor, actor_done_rx) = match inputs.mode {
-                RecordingMode::Studio => {
-                    let (handle, actor_done_rx) = cap_recording::spawn_studio_recording_actor(
-                        id.clone(),
-                        recording_dir.clone(),
-                        base_inputs,
-                        state.camera_feed.clone(),
-                        general_settings
-                            .map(|s| s.custom_cursor_capture)
-                            .unwrap_or_default(),
-                    )
-                    .await
-                    .map_err(|e| {
-                        error!("Failed to spawn studio recording actor: {e}");
-                        e.to_string()
-                    })?;
-
-                    (
-                        InProgressRecording::Studio {
-                            handle,
-                            target_name,
-                            inputs,
-                            recording_dir: recording_dir.clone(),
-                        },
-                        actor_done_rx,
-                    )
-                }
-                RecordingMode::Instant => {
-                    let Some(video_upload_info) = video_upload_info.clone() else {
-                        return Err("Video upload info not found".to_string());
-                    };
+    // done in spawn to catch panics just in case
+    let spawn_actor_res = async {
+        spawn_actor({
+            let state_mtx = Arc::clone(&state_mtx);
+            let general_settings = general_settings.cloned();
+            let capture_target = inputs.capture_target.clone();
+            async move {
+                fail!("recording::spawn_actor");
+                let mut state = state_mtx.write().await;
+
+                let base_inputs = cap_recording::RecordingBaseInputs {
+                    capture_target,
+                    capture_system_audio: inputs.capture_system_audio,
+                    mic_feed: &state.mic_feed,
+                };
 
-                    let (handle, actor_done_rx) =
-                        cap_recording::instant_recording::spawn_instant_recording_actor(
+                let (actor, actor_done_rx) = match inputs.mode {
+                    RecordingMode::Studio => {
+                        let (handle, actor_done_rx) = cap_recording::spawn_studio_recording_actor(
                             id.clone(),
                             recording_dir.clone(),
                             base_inputs,
+                            state.camera_feed.clone(),
+                            general_settings
+                                .map(|s| s.custom_cursor_capture)
+                                .unwrap_or_default(),
                         )
                         .await
                         .map_err(|e| {
@@ -430,27 +376,81 @@ pub async fn start_recording(
                             e.to_string()
                         })?;
 
-                    (
-                        InProgressRecording::Instant {
-                            handle,
-                            progressive_upload,
-                            video_upload_info,
-                            target_name,
-                            inputs,
-                            recording_dir: recording_dir.clone(),
-                        },
-                        actor_done_rx,
-                    )
-                }
-            };
+                        (
+                            InProgressRecording::Studio {
+                                handle,
+                                target_name,
+                                inputs,
+                                recording_dir: recording_dir.clone(),
+                            },
+                            actor_done_rx,
+                        )
+                    }
+                    RecordingMode::Instant => {
+                        let Some(video_upload_info) = video_upload_info.clone() else {
+                            return Err("Video upload info not found".to_string());
+                        };
 
-            state.set_current_recording(actor);
+                        let (handle, actor_done_rx) =
+                            cap_recording::instant_recording::spawn_instant_recording_actor(
+                                id.clone(),
+                                recording_dir.clone(),
+                                base_inputs,
+                            )
+                            .await
+                            .map_err(|e| {
+                                error!("Failed to spawn studio recording actor: {e}");
+                                e.to_string()
+                            })?;
+
+                        (
+                            InProgressRecording::Instant {
+                                handle,
+                                progressive_upload,
+                                video_upload_info,
+                                target_name,
+                                inputs,
+                                recording_dir: recording_dir.clone(),
+                            },
+                            actor_done_rx,
+                        )
+                    }
+                };
+
+                state.set_current_recording(actor);
 
-            Ok::<_, String>(actor_done_rx)
+                Ok::<_, String>(actor_done_rx)
+            }
+        })
+        .await
+        .map_err(|e| format!("Failed to spawn recording actor: {e}"))?
+    }
+    .await;
+
+    let actor_done_rx = match spawn_actor_res {
+        Ok(rx) => rx,
+        Err(e) => {
+            let _ = RecordingEvent::Failed { error: e.clone() }.emit(&app);
+
+            let mut dialog = MessageDialogBuilder::new(
+                app.dialog().clone(),
+                "An error occurred".to_string(),
+                e.clone(),
+            )
+            .kind(tauri_plugin_dialog::MessageDialogKind::Error);
+
+            if let Some(window) = CapWindowId::InProgressRecording.get(&app) {
+                dialog = dialog.parent(&window);
+            }
+
+            dialog.blocking_show();
+
+            let mut state = state_mtx.write().await;
+            let _ = handle_recording_end(app, None, &mut state).await;
+
+            return Err(e);
         }
-    })
-    .await
-    .map_err(|e| format!("Failed to spawn recording actor: {e}"))??;
+    };
 
     let _ = RecordingEvent::Started.emit(&app);
 
@@ -459,7 +459,9 @@ pub async fn start_recording(
         let state_mtx = Arc::clone(&state_mtx);
         async move {
             fail!("recording::wait_actor_done");
-            match actor_done_rx.await {
+            let res = actor_done_rx.await;
+            info!("recording wait actor done: {:?}", &res);
+            match res {
                 Ok(Ok(_)) => {
                     let _ = finish_upload_tx.send(());
                     let _ = RecordingEvent::Stopped.emit(&app);
@@ -486,7 +488,9 @@ pub async fn start_recording(
                     handle_recording_end(app, None, &mut state).await.ok();
                 }
                 // Actor hasn't errored, it's just finished
-                _ => {}
+                v => {
+                    info!("recording actor ended: {v:?}");
+                }
             }
         }
     });
diff --git a/apps/desktop/src-tauri/src/target_select_overlay.rs b/apps/desktop/src-tauri/src/target_select_overlay.rs
index e3bf77e8a0..75165c955b 100644
--- a/apps/desktop/src-tauri/src/target_select_overlay.rs
+++ b/apps/desktop/src-tauri/src/target_select_overlay.rs
@@ -70,17 +70,19 @@ pub async fn open_target_select_overlays(
                     window: window.and_then(|w| {
                         Some(WindowUnderCursor {
                             id: w.id(),
-                            bounds: w.bounds()?,
+                            bounds: w.display_relative_logical_bounds()?,
                             app_name: w.owner_name()?,
                             icon: w.app_icon().map(|bytes| {
                                 format!("data:image/png;base64,{}", BASE64_STANDARD.encode(&bytes))
                             }),
                         })
                     }),
-                    screen: display.map(|d| ScreenUnderCursor {
-                        name: d.name(),
-                        physical_size: d.physical_size(),
-                        refresh_rate: d.refresh_rate().to_string(),
+                    screen: display.and_then(|d| {
+                        Some(ScreenUnderCursor {
+                            name: d.name().unwrap_or_default(),
+                            physical_size: d.physical_size()?,
+                            refresh_rate: d.refresh_rate().to_string(),
+                        })
                     }),
                 }
                 .emit(&app);
diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs
index 1b3e145e1c..d059271b68 100644
--- a/apps/desktop/src-tauri/src/windows.rs
+++ b/apps/desktop/src-tauri/src/windows.rs
@@ -1,14 +1,7 @@
 #![allow(unused_mut)]
 #![allow(unused_imports)]
 
-use crate::{
-    App, ArcLock, fake_window,
-    general_settings::{AppTheme, GeneralSettingsStore},
-    permissions,
-    target_select_overlay::WindowFocusManager,
-};
-use cap_displays::DisplayId;
-use cap_media::{platform::logical_monitor_bounds, sources::CaptureScreen};
+use cap_displays::{Display, DisplayId};
 use futures::pin_mut;
 use serde::Deserialize;
 use specta::Type;
@@ -25,6 +18,13 @@ use tauri::{
 use tokio::sync::RwLock;
 use tracing::debug;
 
+use crate::{
+    App, ArcLock, fake_window,
+    general_settings::{AppTheme, GeneralSettingsStore},
+    permissions,
+    target_select_overlay::WindowFocusManager,
+};
+
 #[cfg(target_os = "macos")]
 const DEFAULT_TRAFFIC_LIGHTS_INSET: LogicalPosition<f64> = LogicalPosition::new(12.0, 12.0);
 
@@ -36,7 +36,7 @@ pub enum CapWindowId {
     Settings,
     Editor { id: u32 },
     RecordingsOverlay,
-    WindowCaptureOccluder { screen_id: u32 },
+    WindowCaptureOccluder { screen_id: DisplayId },
     TargetSelectOverlay { display_id: DisplayId },
     CaptureArea,
     Camera,
@@ -70,7 +70,7 @@ impl FromStr for CapWindowId {
             s if s.starts_with("window-capture-occluder-") => Self::WindowCaptureOccluder {
                 screen_id: s
                     .replace("window-capture-occluder-", "")
-                    .parse::<u32>()
+                    .parse::<DisplayId>()
                     .map_err(|e| e.to_string())?,
             },
             s if s.starts_with("target-select-overlay-") => Self::TargetSelectOverlay {
@@ -180,9 +180,9 @@ pub enum ShowCapWindow {
     Settings { page: Option<String> },
     Editor { project_path: PathBuf },
     RecordingsOverlay,
-    WindowCaptureOccluder { screen_id: u32 },
+    WindowCaptureOccluder { screen_id: DisplayId },
     TargetSelectOverlay { display_id: DisplayId },
-    CaptureArea { screen_id: u32 },
+    CaptureArea { screen_id: DisplayId },
     Camera,
     InProgressRecording { countdown: Option<u32> },
     Upgrade,
@@ -237,6 +237,7 @@ impl ShowCapWindow {
                         .maximizable(false)
                         .always_on_top(true)
                         .visible_on_all_workspaces(true)
+                        .content_protected(true)
                         .center()
                         .build()?;
 
@@ -251,12 +252,19 @@ impl ShowCapWindow {
                 }
             }
             Self::TargetSelectOverlay { display_id } => {
-                let Some(display) = cap_displays::Display::from_id(display_id.clone()) else {
+                let Some(display) = cap_displays::Display::from_id(display_id) else {
                     return Err(tauri::Error::WindowNotFound);
                 };
 
-                let size = display.raw_handle().logical_size();
+                #[cfg(target_os = "macos")]
                 let position = display.raw_handle().logical_position();
+                #[cfg(target_os = "macos")]
+                let size = display.logical_size().unwrap();
+
+                #[cfg(windows)]
+                let position = display.raw_handle().physical_position().unwrap();
+                #[cfg(windows)]
+                let size = display.physical_size().unwrap();
 
                 let mut window_builder = self
                     .window_builder(
@@ -394,10 +402,18 @@ impl ShowCapWindow {
                 window
             }
             Self::WindowCaptureOccluder { screen_id } => {
-                let Some(bounds) = logical_monitor_bounds(*screen_id) else {
+                let Some(display) = Display::from_id(screen_id) else {
                     return Err(tauri::Error::WindowNotFound);
                 };
 
+                #[cfg(target_os = "macos")]
+                let position = display.raw_handle().logical_position();
+
+                #[cfg(windows)]
+                let position = display.raw_handle().physical_position().unwrap();
+
+                let bounds = display.physical_size().unwrap();
+
                 let mut window_builder = self
                     .window_builder(app, "/window-capture-occluder")
                     .maximized(false)
@@ -408,8 +424,8 @@ impl ShowCapWindow {
                     .visible_on_all_workspaces(true)
                     .content_protected(true)
                     .skip_taskbar(true)
-                    .inner_size(bounds.size.width, bounds.size.height)
-                    .position(bounds.position.x, bounds.position.y)
+                    .inner_size(bounds.width(), bounds.height())
+                    .position(position.x(), position.y())
                     .transparent(true);
 
                 let window = window_builder.build()?;
@@ -436,22 +452,23 @@ impl ShowCapWindow {
                     .decorations(false)
                     .transparent(true);
 
-                let screen_bounds = cap_media::platform::monitor_bounds(*screen_id);
-                let target_monitor = app
-                    .monitor_from_point(screen_bounds.x, screen_bounds.y)
-                    .ok()
-                    .flatten()
-                    .unwrap_or(monitor);
-
-                let size = target_monitor.size();
-                let scale_factor = target_monitor.scale_factor();
-                let pos = target_monitor.position();
-                window_builder = window_builder
-                    .inner_size(
-                        (size.width as f64) / scale_factor,
-                        (size.height as f64) / scale_factor,
-                    )
-                    .position(pos.x as f64, pos.y as f64);
+                let Some(display) = Display::from_id(screen_id) else {
+                    return Err(tauri::Error::WindowNotFound);
+                };
+
+                #[cfg(target_os = "macos")]
+                if let Some(bounds) = display.raw_handle().logical_bounds() {
+                    window_builder = window_builder
+                        .inner_size(bounds.size().width(), bounds.size().height())
+                        .position(bounds.position().x(), bounds.position().y());
+                }
+
+                #[cfg(windows)]
+                if let Some(bounds) = display.raw_handle().physical_bounds() {
+                    window_builder = window_builder
+                        .inner_size(bounds.size().width(), bounds.size().height())
+                        .position(bounds.position().x(), bounds.position().y());
+                }
 
                 let window = window_builder.build()?;
 
@@ -465,7 +482,8 @@ impl ShowCapWindow {
                 if let Some(main_window) = CapWindowId::Main.get(app)
                     && let (Ok(outer_pos), Ok(outer_size)) =
                         (main_window.outer_position(), main_window.outer_size())
-                    && target_monitor.intersects(outer_pos, outer_size)
+                    && let Ok(scale_factor) = main_window.scale_factor()
+                    && display.intersects(outer_pos, outer_size, scale_factor)
                 {
                     let _ = main_window.minimize();
                 };
@@ -473,8 +491,7 @@ impl ShowCapWindow {
                 window
             }
             Self::InProgressRecording { countdown } => {
-                let mut width = 180.0 + 32.0;
-
+                let width = 250.0;
                 let height = 40.0;
 
                 let window = self
@@ -626,7 +643,7 @@ impl ShowCapWindow {
             },
             ShowCapWindow::WindowCaptureOccluder { screen_id } => {
                 CapWindowId::WindowCaptureOccluder {
-                    screen_id: *screen_id,
+                    screen_id: screen_id.clone(),
                 }
             }
             ShowCapWindow::CaptureArea { .. } => CapWindowId::CaptureArea,
@@ -715,30 +732,68 @@ fn position_traffic_lights_impl(
 
 // Credits: tauri-plugin-window-state
 trait MonitorExt {
-    fn intersects(&self, position: PhysicalPosition<i32>, size: PhysicalSize<u32>) -> bool;
+    fn intersects(
+        &self,
+        position: PhysicalPosition<i32>,
+        size: PhysicalSize<u32>,
+        scale: f64,
+    ) -> bool;
 }
 
-impl MonitorExt for Monitor {
-    fn intersects(&self, position: PhysicalPosition<i32>, size: PhysicalSize<u32>) -> bool {
-        let PhysicalPosition { x, y } = *self.position();
-        let PhysicalSize { width, height } = *self.size();
-
-        let left = x;
-        let right = x + width as i32;
-        let top = y;
-        let bottom = y + height as i32;
-
-        [
-            (position.x, position.y),
-            (position.x + size.width as i32, position.y),
-            (position.x, position.y + size.height as i32),
-            (
-                position.x + size.width as i32,
-                position.y + size.height as i32,
-            ),
-        ]
-        .into_iter()
-        .any(|(x, y)| x >= left && x < right && y >= top && y < bottom)
+impl MonitorExt for Display {
+    fn intersects(
+        &self,
+        position: PhysicalPosition<i32>,
+        size: PhysicalSize<u32>,
+        _scale: f64,
+    ) -> bool {
+        #[cfg(target_os = "macos")]
+        {
+            let Some(bounds) = self.raw_handle().logical_bounds() else {
+                return false;
+            };
+
+            let left = (bounds.position().x() * _scale) as i32;
+            let right = left + (bounds.size().width() * _scale) as i32;
+            let top = (bounds.position().y() * _scale) as i32;
+            let bottom = top + (bounds.size().height() * _scale) as i32;
+
+            [
+                (position.x, position.y),
+                (position.x + size.width as i32, position.y),
+                (position.x, position.y + size.height as i32),
+                (
+                    position.x + size.width as i32,
+                    position.y + size.height as i32,
+                ),
+            ]
+            .into_iter()
+            .any(|(x, y)| x >= left && x < right && y >= top && y < bottom)
+        }
+
+        #[cfg(windows)]
+        {
+            let Some(bounds) = self.raw_handle().physical_bounds() else {
+                return false;
+            };
+
+            let left = bounds.position().x() as i32;
+            let right = left + bounds.size().width() as i32;
+            let top = bounds.position().y() as i32;
+            let bottom = top + bounds.size().height() as i32;
+
+            [
+                (position.x, position.y),
+                (position.x + size.width as i32, position.y),
+                (position.x, position.y + size.height as i32),
+                (
+                    position.x + size.width as i32,
+                    position.y + size.height as i32,
+                ),
+            ]
+            .into_iter()
+            .any(|(x, y)| x >= left && x < right && y >= top && y < bottom)
+        }
     }
 }
 
diff --git a/apps/desktop/src/components/CropAreaRenderer.tsx b/apps/desktop/src/components/CropAreaRenderer.tsx
index 13481af8f8..b40e0d7adb 100644
--- a/apps/desktop/src/components/CropAreaRenderer.tsx
+++ b/apps/desktop/src/components/CropAreaRenderer.tsx
@@ -6,11 +6,11 @@ import {
 	type ParentProps,
 } from "solid-js";
 import { createHiDPICanvasContext } from "~/utils/canvas";
-import type { Bounds } from "~/utils/tauri";
+import type { LogicalBounds } from "~/utils/tauri";
 
 type DrawContext = {
 	ctx: CanvasRenderingContext2D;
-	bounds: Bounds;
+	bounds: LogicalBounds;
 	radius: number;
 	prefersDark: boolean;
 	highlighted: boolean;
@@ -24,7 +24,10 @@ function drawHandles({
 	highlighted,
 	selected,
 }: DrawContext) {
-	const { x, y, width, height } = bounds;
+	const {
+		position: { x, y },
+		size: { width, height },
+	} = bounds;
 	const minSizeForSideHandles = 100;
 
 	ctx.strokeStyle = selected
@@ -82,44 +85,36 @@ function drawHandles({
 	// Center handles
 	const handleLength = 35;
 	const sideHandleDistance = 0;
-	const centerX = bounds.x + bounds.width / 2;
-	const centerY = bounds.y + bounds.height / 2;
+	const centerX = x + width / 2;
+	const centerY = y + height / 2;
 
 	ctx.beginPath();
 
 	// top center
-	ctx.moveTo(centerX - handleLength / 2, bounds.y - sideHandleDistance);
-	ctx.lineTo(centerX + handleLength / 2, bounds.y - sideHandleDistance);
+	ctx.moveTo(centerX - handleLength / 2, y - sideHandleDistance);
+	ctx.lineTo(centerX + handleLength / 2, y - sideHandleDistance);
 
 	// bottom center
-	ctx.moveTo(
-		centerX - handleLength / 2,
-		bounds.y + bounds.height + sideHandleDistance,
-	);
-	ctx.lineTo(
-		centerX + handleLength / 2,
-		bounds.y + bounds.height + sideHandleDistance,
-	);
+	ctx.moveTo(centerX - handleLength / 2, y + height + sideHandleDistance);
+	ctx.lineTo(centerX + handleLength / 2, y + height + sideHandleDistance);
 
 	// left center
-	ctx.moveTo(bounds.x - sideHandleDistance, centerY - handleLength / 2);
-	ctx.lineTo(bounds.x - sideHandleDistance, centerY + handleLength / 2);
+	ctx.moveTo(x - sideHandleDistance, centerY - handleLength / 2);
+	ctx.lineTo(x - sideHandleDistance, centerY + handleLength / 2);
 
 	// right center
-	ctx.moveTo(
-		bounds.x + bounds.width + sideHandleDistance,
-		centerY - handleLength / 2,
-	);
-	ctx.lineTo(
-		bounds.x + bounds.width + sideHandleDistance,
-		centerY + handleLength / 2,
-	);
+	ctx.moveTo(x + width + sideHandleDistance, centerY - handleLength / 2);
+	ctx.lineTo(x + width + sideHandleDistance, centerY + handleLength / 2);
 
 	ctx.stroke();
 }
 
 // Rule of thirds guide lines and center crosshair
-function drawGuideLines({ ctx, bounds, prefersDark }: DrawContext) {
+function drawGuideLines({
+	ctx,
+	bounds: { position, size },
+	prefersDark,
+}: DrawContext) {
 	ctx.strokeStyle = prefersDark
 		? "rgba(255, 255, 255, 0.5)"
 		: "rgba(0, 0, 0, 0.5)";
@@ -129,23 +124,23 @@ function drawGuideLines({ ctx, bounds, prefersDark }: DrawContext) {
 	// Rule of thirds
 	ctx.beginPath();
 	for (let i = 1; i < 3; i++) {
-		const x = bounds.x + (bounds.width * i) / 3;
-		ctx.moveTo(x, bounds.y);
-		ctx.lineTo(x, bounds.y + bounds.height);
+		const x = position.x + (size.width * i) / 3;
+		ctx.moveTo(x, position.y);
+		ctx.lineTo(x, position.y + size.height);
 	}
 	ctx.stroke();
 
 	ctx.beginPath();
 	for (let i = 1; i < 3; i++) {
-		const y = bounds.y + (bounds.height * i) / 3;
-		ctx.moveTo(bounds.x, y);
-		ctx.lineTo(bounds.x + bounds.width, y);
+		const y = position.y + (size.height * i) / 3;
+		ctx.moveTo(position.x, y);
+		ctx.lineTo(position.x + size.width, y);
 	}
 	ctx.stroke();
 
 	// Center crosshair
-	const centerX = Math.round(bounds.x + bounds.width / 2);
-	const centerY = Math.round(bounds.y + bounds.height / 2);
+	const centerX = Math.round(position.x + size.width / 2);
+	const centerY = Math.round(position.y + size.height / 2);
 
 	ctx.setLineDash([]);
 	ctx.lineWidth = 2;
@@ -165,7 +160,7 @@ function drawGuideLines({ ctx, bounds, prefersDark }: DrawContext) {
 // Main draw function
 function draw(
 	ctx: CanvasRenderingContext2D,
-	bounds: Bounds,
+	{ position, size }: LogicalBounds,
 	radius: number,
 	guideLines: boolean,
 	showHandles: boolean,
@@ -173,10 +168,10 @@ function draw(
 	selected: boolean,
 	prefersDark: boolean,
 ) {
-	if (bounds.width <= 0 || bounds.height <= 0) return;
+	if (size.width <= 0 || size.height <= 0) return;
 	const drawContext: DrawContext = {
 		ctx,
-		bounds,
+		bounds: { position, size },
 		radius,
 		prefersDark,
 		highlighted,
@@ -195,16 +190,16 @@ function draw(
 	ctx.shadowBlur = 200;
 	ctx.shadowOffsetY = 25;
 	ctx.beginPath();
-	ctx.roundRect(bounds.x, bounds.y, bounds.width, bounds.height, radius);
+	ctx.roundRect(position.x, position.y, size.width, size.height, radius);
 	ctx.fill();
 	ctx.restore();
 
 	if (showHandles) drawHandles(drawContext);
 
 	ctx.beginPath();
-	ctx.roundRect(bounds.x, bounds.y, bounds.width, bounds.height, radius);
+	ctx.roundRect(position.x, position.y, size.width, size.height, radius);
 	ctx.clip();
-	ctx.clearRect(bounds.x, bounds.y, bounds.width, bounds.height);
+	ctx.clearRect(position.x, position.y, size.width, size.height);
 
 	if (guideLines) drawGuideLines(drawContext);
 
@@ -213,7 +208,7 @@ function draw(
 
 export default function CropAreaRenderer(
 	props: ParentProps<{
-		bounds: Bounds;
+		bounds: LogicalBounds;
 		guideLines?: boolean;
 		handles?: boolean;
 		borderRadius?: number;
@@ -255,15 +250,20 @@ export default function CropAreaRenderer(
 		createEffect(() => {
 			if (lastAnimationFrameId) cancelAnimationFrame(lastAnimationFrameId);
 
-			const { x, y, width, height } = props.bounds;
-			const { guideLines, handles, borderRadius, highlighted, selected } =
-				props;
+			const {
+				guideLines,
+				handles,
+				borderRadius,
+				highlighted,
+				selected,
+				bounds: { ...bounds },
+			} = props;
 
 			const prefersDark = prefersDarkScheme();
 			lastAnimationFrameId = requestAnimationFrame(() =>
 				draw(
 					ctx,
-					{ x, y, width, height },
+					bounds,
 					borderRadius || 0,
 					guideLines || false,
 					handles || false,
diff --git a/apps/desktop/src/components/Cropper.tsx b/apps/desktop/src/components/Cropper.tsx
index bcc0e9ae4a..88379b2213 100644
--- a/apps/desktop/src/components/Cropper.tsx
+++ b/apps/desktop/src/components/Cropper.tsx
@@ -96,7 +96,8 @@ export default function Cropper(
 		showGuideLines?: boolean;
 	}>,
 ) {
-	const crop = props.value;
+	const position = () => props.value.position;
+	const size = () => props.value.size;
 
 	const [containerSize, setContainerSize] = createSignal({ x: 0, y: 0 });
 	const mappedSize = createMemo(() => props.mappedSize || containerSize());
@@ -121,10 +122,10 @@ export default function Cropper(
 		const mapped = mappedSize();
 		const container = containerSize();
 		return {
-			x: (crop.position.x / mapped.x) * container.x,
-			y: (crop.position.y / mapped.y) * container.y,
-			width: (crop.size.x / mapped.x) * container.x,
-			height: (crop.size.y / mapped.y) * container.y,
+			x: (position().x / mapped.x) * container.x,
+			y: (position().y / mapped.y) * container.y,
+			width: (size().x / mapped.x) * container.x,
+			height: (size().y / mapped.y) * container.y,
 		};
 	});
 
@@ -173,7 +174,7 @@ export default function Cropper(
 			() => props.aspectRatio,
 			() => {
 				if (!props.aspectRatio) return;
-				const box = Box.from(crop.position, crop.size);
+				const box = Box.from(position(), size());
 				box.constrainToRatio(props.aspectRatio, ORIGIN_CENTER);
 				box.constrainToBoundary(mappedSize().x, mappedSize().y, ORIGIN_CENTER);
 				setCrop(box.toBounds());
@@ -204,7 +205,7 @@ export default function Cropper(
 		event.stopPropagation();
 		setDragging(true);
 		let lastValidPos = { x: event.clientX, y: event.clientY };
-		const box = Box.from(crop.position, crop.size);
+		const box = Box.from(position(), size());
 		const scaleFactors = containerToMappedSizeScale();
 
 		createRoot((dispose) => {
@@ -225,7 +226,7 @@ export default function Cropper(
 						);
 
 						const newBox = box;
-						if (newBox.x !== crop.position.x || newBox.y !== crop.position.y) {
+						if (newBox.x !== position().x || newBox.y !== position().y) {
 							lastValidPos = { x: e.clientX, y: e.clientY };
 							setCrop(newBox.toBounds());
 						}
@@ -237,7 +238,7 @@ export default function Cropper(
 
 	function handleWheel(event: WheelEvent) {
 		event.preventDefault();
-		const box = Box.from(crop.position, crop.size);
+		const box = Box.from(position(), size());
 		const mapped = mappedSize();
 
 		if (event.ctrlKey) {
@@ -281,8 +282,8 @@ export default function Cropper(
 			batch(() => {
 				setGestureState("initialPinchDistance", distance);
 				setGestureState("initialSize", {
-					width: crop.size.x,
-					height: crop.size.y,
+					width: size().x,
+					height: size().y,
 				});
 				setGestureState("lastTouchCenter", { x: centerX, y: centerY });
 			});
@@ -304,11 +305,11 @@ export default function Cropper(
 			const currentDistance = distanceOf(event.touches[0], event.touches[1]);
 			const scale = currentDistance / gestureState.initialPinchDistance;
 
-			const box = Box.from(crop.position, crop.size);
+			const box = Box.from(position(), size());
 			const mapped = mappedSize();
 
 			// Calculate new dimensions while maintaining aspect ratio
-			const currentRatio = crop.size.x / crop.size.y;
+			const currentRatio = size().x / size().y;
 			let newWidth = clamp(
 				gestureState.initialSize.width * scale,
 				minSize().x,
@@ -344,7 +345,7 @@ export default function Cropper(
 			setCrop(box.toBounds());
 		} else if (event.touches.length === 1 && dragging()) {
 			// Handle single touch drag
-			const box = Box.from(crop.position, crop.size);
+			const box = Box.from(position(), size());
 			const scaleFactors = containerToMappedSizeScale();
 			const mapped = mappedSize();
 
@@ -414,7 +415,7 @@ export default function Cropper(
 		};
 
 		let lastValidPos = { x: clientX, y: clientY };
-		const box = Box.from(crop.position, crop.size);
+		const box = Box.from(position(), size());
 		const scaleFactors = containerToMappedSizeScale();
 		const mapped = mappedSize();
 
@@ -504,10 +505,10 @@ export default function Cropper(
 
 			const newBox = box.toBounds();
 			if (
-				newBox.size.x !== crop.size.x ||
-				newBox.size.y !== crop.size.y ||
-				newBox.position.x !== crop.position.x ||
-				newBox.position.y !== crop.position.y
+				newBox.size.x !== size().x ||
+				newBox.size.y !== size().y ||
+				newBox.position.x !== position().x ||
+				newBox.position.y !== position().y
 			) {
 				lastValidPos = { x: moveX, y: moveY };
 				props.onCropChange(newBox);
@@ -530,7 +531,7 @@ export default function Cropper(
 
 		if (lastKeyHandleFrame) return;
 		lastKeyHandleFrame = requestAnimationFrame(() => {
-			const box = Box.from(crop.position, crop.size);
+			const box = Box.from(position(), size());
 			const mapped = mappedSize();
 			const scaleFactors = containerToMappedSizeScale();
 
@@ -599,7 +600,6 @@ export default function Cropper(
 
 	return (
 		<div
-			aria-label="Crop area"
 			ref={containerRef}
 			class={`relative h-full w-full overflow-hidden overscroll-contain *:overscroll-none ${props.class}`}
 			onWheel={handleWheel}
@@ -608,30 +608,34 @@ export default function Cropper(
 			onTouchEnd={handleTouchEnd}
 			onKeyDown={handleKeyDown}
 			tabIndex={0}
-			onContextMenu={async (e) => {
-				e.preventDefault();
-				const menu = await Menu.new({
-					id: "crop-options",
-					items: [
-						{
-							id: "enableRatioSnap",
-							text: "Snap to aspect ratios",
-							checked: snapToRatioEnabled(),
-							action: () => {
-								setSnapToRatioEnabled((v) => !v);
-							},
-						} satisfies CheckMenuItemOptions,
-					],
-				});
-				menu.popup();
-			}}
+			// onContextMenu={async (e) => {
+			// 	e.preventDefault();
+			// 	const menu = await Menu.new({
+			// 		id: "crop-options",
+			// 		items: [
+			// 			{
+			// 				id: "enableRatioSnap",
+			// 				text: "Snap to aspect ratios",
+			// 				checked: snapToRatioEnabled(),
+			// 				action: () => {
+			// 					setSnapToRatioEnabled((v) => !v);
+			// 				},
+			// 			} satisfies CheckMenuItemOptions,
+			// 		],
+			// 	});
+			// 	menu.popup();
+			// }}
 		>
 			<CropAreaRenderer
 				bounds={{
-					x: displayScaledCrop().x,
-					y: displayScaledCrop().y,
-					width: displayScaledCrop().width,
-					height: displayScaledCrop().height,
+					position: {
+						x: displayScaledCrop().x,
+						y: displayScaledCrop().y,
+					},
+					size: {
+						width: displayScaledCrop().width,
+						height: displayScaledCrop().height,
+					},
 				}}
 				borderRadius={9}
 				guideLines={props.showGuideLines}
diff --git a/apps/desktop/src/routes/(window-chrome)/(main).tsx b/apps/desktop/src/routes/(window-chrome)/(main).tsx
index 188a93f7e6..30bf74ca9c 100644
--- a/apps/desktop/src/routes/(window-chrome)/(main).tsx
+++ b/apps/desktop/src/routes/(window-chrome)/(main).tsx
@@ -36,7 +36,7 @@ import {
 } from "~/utils/queries";
 import {
 	type CameraInfo,
-	type CaptureScreen,
+	type CaptureDisplay,
 	type CaptureWindow,
 	commands,
 	events,
@@ -143,9 +143,9 @@ function Page() {
 	// allowing us to define fallbacks if the selected options aren't actually available
 	const options = {
 		screen: () => {
-			let screen;
+			let screen: CaptureDisplay | undefined;
 
-			if (rawOptions.captureTarget.variant === "screen") {
+			if (rawOptions.captureTarget.variant === "display") {
 				const screenId = rawOptions.captureTarget.id;
 				screen = _screens()?.find((s) => s.id === screenId) ?? _screens()?.[0];
 			} else if (rawOptions.captureTarget.variant === "area") {
@@ -156,7 +156,7 @@ function Page() {
 			return screen;
 		},
 		window: () => {
-			let win;
+			let win: CaptureWindow | undefined;
 
 			if (rawOptions.captureTarget.variant === "window") {
 				const windowId = rawOptions.captureTarget.id;
@@ -170,12 +170,16 @@ function Page() {
 				const { cameraID } = rawOptions;
 				if (!cameraID) return;
 				if ("ModelID" in cameraID && c.model_id === cameraID.ModelID) return c;
-				if ("DeviceID" in cameraID && c.device_id == cameraID.DeviceID)
+				if ("DeviceID" in cameraID && c.device_id === cameraID.DeviceID)
 					return c;
 			}),
 		micName: () => mics.data?.find((name) => name === rawOptions.micName),
 	};
 
+	createEffect(() => {
+		console.log(_windows());
+	});
+
 	// if target is window and no windows are available, switch to screen capture
 	createEffect(() => {
 		const screen = _screens()?.[0];
@@ -187,7 +191,7 @@ function Page() {
 		) {
 			setOptions(
 				"captureTarget",
-				reconcile({ variant: "screen", id: screen.id }),
+				reconcile({ variant: "display", id: screen.id }),
 			);
 		}
 	});
@@ -197,7 +201,7 @@ function Page() {
 			if (!isRecording()) {
 				const capture_target = ((): ScreenCaptureTarget => {
 					switch (rawOptions.captureTarget.variant) {
-						case "screen": {
+						case "display": {
 							const screen = options.screen();
 							if (!screen)
 								throw new Error(
@@ -205,7 +209,7 @@ function Page() {
 										_screens()?.length
 									}`,
 								);
-							return { variant: "screen", id: screen.id };
+							return { variant: "display", id: screen.id };
 						}
 						case "window": {
 							const win = options.window();
@@ -366,12 +370,14 @@ function Page() {
 							: rawOptions.captureTarget.variant
 					}
 					onChange={(area) => {
+						const screen = options.screen();
+						if (!screen) return;
 						if (!area)
 							setOptions(
 								"captureTarget",
 								reconcile({
-									variant: "screen",
-									id: options.screen()?.id ?? -1,
+									variant: "display",
+									id: screen.id,
 								}),
 							);
 					}}
@@ -379,7 +385,7 @@ function Page() {
 				<div
 					class={cx(
 						"flex flex-row items-center rounded-[0.5rem] relative border h-8 transition-all duration-500",
-						(rawOptions.captureTarget.variant === "screen" ||
+						(rawOptions.captureTarget.variant === "display" ||
 							rawOptions.captureTarget.variant === "area") &&
 							"ml-[2.4rem]",
 					)}
@@ -399,7 +405,7 @@ function Page() {
 					>
 						<div class="flex-1 bg-gray-2" />
 					</div>
-					<TargetSelect<CaptureScreen>
+					<TargetSelect<CaptureDisplay>
 						options={_screens() ?? []}
 						onChange={(value) => {
 							if (!value) return;
@@ -412,14 +418,14 @@ function Page() {
 
 							setOptions(
 								"captureTarget",
-								reconcile({ variant: "screen", id: value.id }),
+								reconcile({ variant: "display", id: value.id }),
 							);
 						}}
 						value={options.screen() ?? null}
-						placeholder="Screen"
+						placeholder="Display"
 						optionsEmptyText="No screens found"
 						selected={
-							rawOptions.captureTarget.variant === "screen" ||
+							rawOptions.captureTarget.variant === "display" ||
 							rawOptions.captureTarget.variant === "area"
 						}
 					/>
@@ -585,8 +591,8 @@ function createUpdateCheck() {
 }
 
 function AreaSelectButton(props: {
-	targetVariant: "screen" | "area" | "other";
-	screen: CaptureScreen | undefined;
+	targetVariant: "display" | "area" | "other";
+	screen: CaptureDisplay | undefined;
 	onChange(area?: number): void;
 }) {
 	const [areaSelection, setAreaSelection] = createStore({ pending: false });
@@ -750,6 +756,7 @@ function CameraSelect(props: {
 	return (
 		<div class="flex flex-col gap-[0.25rem] items-stretch text-[--text-primary]">
 			<button
+				type="button"
 				disabled={!!currentRecording.data || props.disabled}
 				class="flex flex-row items-center h-[2rem] px-[0.375rem] gap-[0.375rem] border rounded-lg border-gray-3 w-full disabled:text-gray-11 transition-colors KSelect"
 				onClick={() => {
@@ -856,6 +863,7 @@ function MicrophoneSelect(props: {
 	return (
 		<div class="flex flex-col gap-[0.25rem] items-stretch text-[--text-primary]">
 			<button
+				type="button"
 				disabled={!!currentRecording.data || props.disabled}
 				class="relative flex flex-row items-center h-[2rem] px-[0.375rem] gap-[0.375rem] border rounded-lg border-gray-3 w-full disabled:text-gray-11 transition-colors KSelect overflow-hidden z-10"
 				onClick={() => {
@@ -921,6 +929,7 @@ function SystemAudio() {
 
 	return (
 		<button
+			type="button"
 			onClick={() => {
 				if (!rawOptions) return;
 				setOptions({ captureSystemAudio: !rawOptions.captureSystemAudio });
@@ -943,7 +952,7 @@ function SystemAudio() {
 	);
 }
 
-function TargetSelect<T extends { id: number; name: string }>(props: {
+function TargetSelect<T extends { id: string; name: string }>(props: {
 	options: Array<T>;
 	onChange: (value: T) => void;
 	value: T | null;
@@ -969,6 +978,7 @@ function TargetSelect<T extends { id: number; name: string }>(props: {
 
 	return (
 		<button
+			type="button"
 			class="group flex-1 text-gray-11 py-1 z-10 data-[selected='true']:text-gray-12 disabled:text-gray-10 peer focus:outline-none transition-colors duration-100 w-full text-nowrap overflow-hidden px-2 flex gap-2 items-center justify-center"
 			data-selected={props.selected}
 			disabled={props.disabled}
diff --git a/apps/desktop/src/routes/(window-chrome)/new-main.tsx b/apps/desktop/src/routes/(window-chrome)/new-main.tsx
index af5817273e..b1ac198683 100644
--- a/apps/desktop/src/routes/(window-chrome)/new-main.tsx
+++ b/apps/desktop/src/routes/(window-chrome)/new-main.tsx
@@ -37,7 +37,6 @@ import {
 } from "~/utils/queries";
 import {
 	type CameraInfo,
-	type CaptureScreen,
 	commands,
 	type DeviceOrModelID,
 	events,
@@ -147,7 +146,7 @@ function Page() {
 		screen: () => {
 			let screen;
 
-			if (rawOptions.captureTarget.variant === "screen") {
+			if (rawOptions.captureTarget.variant === "display") {
 				const screenId = rawOptions.captureTarget.id;
 				screen =
 					screens.data?.find((s) => s.id === screenId) ?? screens.data?.[0];
@@ -173,33 +172,43 @@ function Page() {
 			if (!rawOptions.cameraID) return undefined;
 			return findCamera(cameras.data || [], rawOptions.cameraID);
 		},
-
 		micName: () => mics.data?.find((name) => name === rawOptions.micName),
-		target: (): ScreenCaptureTarget => {
+		target: (): ScreenCaptureTarget | undefined => {
 			switch (rawOptions.captureTarget.variant) {
-				case "screen":
-					return { variant: "screen", id: options.screen()?.id ?? -1 };
-				case "window":
-					return { variant: "window", id: options.window()?.id ?? -1 };
-				case "area":
+				case "display": {
+					const screen = options.screen();
+					if (!screen) return;
+					return { variant: "display", id: screen.id };
+				}
+				case "window": {
+					const window = options.window();
+					if (!window) return;
+					return { variant: "window", id: window.id };
+				}
+				case "area": {
+					const screen = options.screen();
+					if (!screen) return;
 					return {
 						variant: "area",
 						bounds: rawOptions.captureTarget.bounds,
-						screen: options.screen()?.id ?? -1,
+						screen: screen.id,
 					};
+				}
 			}
 		},
 	};
 
 	// if target is window and no windows are available, switch to screen capture
 	createEffect(() => {
-		if (options.target().variant === "window" && windows.data?.length === 0) {
+		const target = options.target();
+		if (!target) return;
+		const screen = options.screen();
+		if (!screen) return;
+
+		if (target.variant === "window" && windows.data?.length === 0) {
 			setOptions(
 				"captureTarget",
-				reconcile({
-					variant: "screen",
-					id: options.screen()?.id ?? -1,
-				}),
+				reconcile({ variant: "display", id: screen.id }),
 			);
 		}
 	});
@@ -284,12 +293,14 @@ function Page() {
 			</WindowChromeHeader>
 			<div class="flex flex-row items-stretch gap-1.5 w-full text-xs text-gray-11">
 				<TargetTypeButton
-					selected={rawOptions.targetMode === "screen"}
+					selected={rawOptions.targetMode === "display"}
 					Component={IconMdiMonitor}
 					onClick={() =>
-						setOptions("targetMode", (v) => (v === "screen" ? null : "screen"))
+						setOptions("targetMode", (v) =>
+							v === "display" ? null : "display",
+						)
 					}
-					name="Screen"
+					name="Display"
 				/>
 				<TargetTypeButton
 					selected={rawOptions.targetMode === "window"}
diff --git a/apps/desktop/src/routes/capture-area.tsx b/apps/desktop/src/routes/capture-area.tsx
index 72090353fa..5a00adda96 100644
--- a/apps/desktop/src/routes/capture-area.tsx
+++ b/apps/desktop/src/routes/capture-area.tsx
@@ -56,7 +56,7 @@ export default function CaptureArea() {
 
 	async function handleConfirm() {
 		const target = rawOptions.captureTarget;
-		if (target.variant !== "screen") return;
+		if (target.variant !== "display") return;
 		setPendingState(false);
 
 		setOptions(
@@ -65,10 +65,14 @@ export default function CaptureArea() {
 				variant: "area",
 				screen: target.id,
 				bounds: {
-					x: crop.position.x,
-					y: crop.position.y,
-					width: crop.size.x,
-					height: crop.size.y,
+					position: {
+						x: crop.position.x,
+						y: crop.position.y,
+					},
+					size: {
+						width: crop.size.x,
+						height: crop.size.y,
+					},
 				},
 			}),
 		);
diff --git a/apps/desktop/src/routes/editor/ExportDialog.tsx b/apps/desktop/src/routes/editor/ExportDialog.tsx
index e119bb0e03..094a6a7958 100644
--- a/apps/desktop/src/routes/editor/ExportDialog.tsx
+++ b/apps/desktop/src/routes/editor/ExportDialog.tsx
@@ -33,6 +33,8 @@ import {
 	type ExportSettings,
 	events,
 	type FramesRendered,
+	type GifExportSettings,
+	type Mp4ExportSettings,
 } from "~/utils/tauri";
 import { type RenderState, useEditorContext } from "./context";
 import { RESOLUTION_OPTIONS } from "./Header";
@@ -140,7 +142,7 @@ export function ExportDialog() {
 					y: settings.resolution.height,
 				},
 				compression: settings.compression,
-			},
+			} as (Mp4ExportSettings & GifExportSettings) & { format: "Mp4" | "Gif" },
 			onProgress,
 		);
 
diff --git a/apps/desktop/src/routes/editor/Header.tsx b/apps/desktop/src/routes/editor/Header.tsx
index ffd30fe0fe..65a85a12bb 100644
--- a/apps/desktop/src/routes/editor/Header.tsx
+++ b/apps/desktop/src/routes/editor/Header.tsx
@@ -86,7 +86,10 @@ export function Header() {
 					leftIcon={<IconCapTrash class="w-5" />}
 				/>
 				<EditorButton
-					onClick={() => revealItemInDir(`${editorInstance.path}/`)}
+					onClick={() => {
+						console.log({ path: `${editorInstance.path}/` });
+						revealItemInDir(`${editorInstance.path}/`);
+					}}
 					tooltipText="Open recording bundle"
 					leftIcon={<IconLucideFolder class="w-5" />}
 				/>
diff --git a/apps/desktop/src/routes/recordings-overlay.tsx b/apps/desktop/src/routes/recordings-overlay.tsx
index 2980354cd6..b7a926a99a 100644
--- a/apps/desktop/src/routes/recordings-overlay.tsx
+++ b/apps/desktop/src/routes/recordings-overlay.tsx
@@ -533,10 +533,14 @@ function createFakeWindowBounds(
 
 	createEffect(() => {
 		commands.setFakeWindowBounds(key(), {
-			x: bounds.left ?? 0,
-			y: bounds.top ?? 0,
-			width: bounds.width ?? 0,
-			height: bounds.height ?? 0,
+			position: {
+				x: bounds.left ?? 0,
+				y: bounds.top ?? 0,
+			},
+			size: {
+				width: bounds.width ?? 0,
+				height: bounds.height ?? 0,
+			},
 		});
 	});
 
diff --git a/apps/desktop/src/routes/target-select-overlay.tsx b/apps/desktop/src/routes/target-select-overlay.tsx
index 3bbc4ce9a6..9d5be794a9 100644
--- a/apps/desktop/src/routes/target-select-overlay.tsx
+++ b/apps/desktop/src/routes/target-select-overlay.tsx
@@ -21,13 +21,15 @@ import { createStore, reconcile } from "solid-js/store";
 import { createOptionsQuery } from "~/utils/queries";
 import {
 	commands,
+	type DisplayId,
 	events,
+	type LogicalBounds,
 	type ScreenCaptureTarget,
 	type TargetUnderCursor,
 } from "~/utils/tauri";
 
 export default function () {
-	const [params] = useSearchParams<{ displayId: string }>();
+	const [params] = useSearchParams<{ displayId: DisplayId }>();
 	const { rawOptions, setOptions } = createOptionsQuery();
 
 	const [targetUnderCursor, setTargetUnderCursor] =
@@ -42,7 +44,7 @@ export default function () {
 	});
 	onCleanup(() => unsubTargetUnderCursor.then((unsub) => unsub()));
 
-	const [bounds, _setBounds] = createStore({
+	const [bounds, _setBounds] = createStore<LogicalBounds>({
 		position: { x: 0, y: 0 },
 		size: { width: 400, height: 300 },
 	});
@@ -78,7 +80,7 @@ export default function () {
 
 	return (
 		<Switch>
-			<Match when={rawOptions.targetMode === "screen"}>
+			<Match when={rawOptions.targetMode === "display"}>
 				{(_) => (
 					<Show when={targetUnderCursor.screen} keyed>
 						{(screenUnderCursor) => (
@@ -94,10 +96,7 @@ export default function () {
 								</span>
 
 								<RecordingControls
-									target={{
-										variant: "screen",
-										id: getDisplayId(params.displayId),
-									}}
+									target={{ variant: "display", id: params.displayId! }}
 								/>
 							</div>
 						)}
@@ -145,7 +144,7 @@ export default function () {
 								<RecordingControls
 									target={{
 										variant: "window",
-										id: Number(windowUnderCursor.id),
+										id: windowUnderCursor.id,
 									}}
 								/>
 
@@ -535,13 +534,8 @@ export default function () {
 									<RecordingControls
 										target={{
 											variant: "area",
-											screen: Number(params.displayId),
-											bounds: {
-												x: bounds.position.x,
-												y: bounds.position.y,
-												width: bounds.size.width,
-												height: bounds.size.height,
-											},
+											screen: params.displayId!,
+											bounds,
 										}}
 									/>
 								</div>
diff --git a/apps/desktop/src/routes/window-capture-occluder.tsx b/apps/desktop/src/routes/window-capture-occluder.tsx
index 7b31f931fc..a3ae4a02a9 100644
--- a/apps/desktop/src/routes/window-capture-occluder.tsx
+++ b/apps/desktop/src/routes/window-capture-occluder.tsx
@@ -25,10 +25,6 @@ export default function () {
 		}
 	};
 
-	const [scale] = createResource(() => getCurrentWindow().scaleFactor(), {
-		initialValue: 0,
-	});
-
 	return (
 		<Suspense>
 			<Show when={bounds()}>
@@ -42,16 +38,7 @@ export default function () {
 
 					return (
 						<CropAreaRenderer
-							bounds={
-								ostype() === "macos"
-									? bounds()
-									: {
-											x: bounds().x / scale(),
-											y: bounds().y / scale(),
-											width: bounds().width / scale(),
-											height: bounds().height / scale(),
-										}
-							}
+							bounds={bounds()}
 							// no border radius as that should be added in editor
 						/>
 					);
diff --git a/apps/desktop/src/utils/queries.ts b/apps/desktop/src/utils/queries.ts
index b26713f014..2e0fa67f19 100644
--- a/apps/desktop/src/utils/queries.ts
+++ b/apps/desktop/src/utils/queries.ts
@@ -16,7 +16,6 @@ import {
 	type CameraInfo,
 	commands,
 	type DeviceOrModelID,
-	ModelIDType,
 	type RecordingMode,
 	type ScreenCaptureTarget,
 } from "./tauri";
@@ -40,8 +39,8 @@ export const listWindows = queryOptions({
 });
 
 export const listScreens = queryOptions({
-	queryKey: ["capture", "screens"] as const,
-	queryFn: () => commands.listCaptureScreens(),
+	queryKey: ["capture", "displays"] as const,
+	queryFn: () => commands.listCaptureDisplays(),
 	reconcile: "id",
 	refetchInterval: 1000,
 });
@@ -86,18 +85,18 @@ export const getPermissions = queryOptions({
 });
 
 export function createOptionsQuery() {
-	const PERSIST_KEY = "recording-options-query";
+	const PERSIST_KEY = "recording-options-query-2";
 	const [_state, _setState] = createStore<{
 		captureTarget: ScreenCaptureTarget;
 		micName: string | null;
 		mode: RecordingMode;
 		captureSystemAudio?: boolean;
-		targetMode?: "screen" | "window" | "area" | null;
+		targetMode?: "display" | "window" | "area" | null;
 		cameraID?: DeviceOrModelID | null;
 		/** @deprecated */
 		cameraLabel: string | null;
 	}>({
-		captureTarget: { variant: "screen", id: 0 },
+		captureTarget: { variant: "display", id: "0" },
 		micName: null,
 		cameraLabel: null,
 		mode: "studio",
diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts
index 84c1629a1f..41170046ff 100644
--- a/apps/desktop/src/utils/tauri.ts
+++ b/apps/desktop/src/utils/tauri.ts
@@ -33,8 +33,8 @@ export const commands = {
 	async listCaptureWindows(): Promise<CaptureWindow[]> {
 		return await TAURI_INVOKE("list_capture_windows");
 	},
-	async listCaptureScreens(): Promise<CaptureScreen[]> {
-		return await TAURI_INVOKE("list_capture_screens");
+	async listCaptureDisplays(): Promise<CaptureDisplay[]> {
+		return await TAURI_INVOKE("list_capture_displays");
 	},
 	async takeScreenshot(): Promise<null> {
 		return await TAURI_INVOKE("take_screenshot");
@@ -45,7 +45,10 @@ export const commands = {
 	async closeRecordingsOverlayWindow(): Promise<void> {
 		await TAURI_INVOKE("close_recordings_overlay_window");
 	},
-	async setFakeWindowBounds(name: string, bounds: Bounds): Promise<null> {
+	async setFakeWindowBounds(
+		name: string,
+		bounds: LogicalBounds,
+	): Promise<null> {
 		return await TAURI_INVOKE("set_fake_window_bounds", { name, bounds });
 	},
 	async removeFakeWindow(name: string): Promise<null> {
@@ -404,7 +407,6 @@ export type BackgroundSource =
 			to: [number, number, number];
 			angle?: number;
 	  };
-export type Bounds = { x: number; y: number; width: number; height: number };
 export type Camera = {
 	hide: boolean;
 	mirror: boolean;
@@ -460,12 +462,16 @@ export type CaptionsData = {
 	segments: CaptionSegment[];
 	settings: CaptionSettings;
 };
-export type CaptureScreen = { id: number; name: string; refresh_rate: number };
+export type CaptureDisplay = {
+	id: DisplayId;
+	name: string;
+	refresh_rate: number;
+};
 export type CaptureWindow = {
-	id: number;
+	id: WindowId;
 	owner_name: string;
 	name: string;
-	bounds: Bounds;
+	bounds: LogicalBounds;
 	refresh_rate: number;
 };
 export type CommercialLicense = {
@@ -481,9 +487,9 @@ export type CurrentRecording = {
 };
 export type CurrentRecordingChanged = null;
 export type CurrentRecordingTarget =
-	| { window: { id: number; bounds: Bounds } }
-	| { screen: { id: number } }
-	| { area: { screen: number; bounds: Bounds } };
+	| { window: { id: WindowId; bounds: LogicalBounds } }
+	| { screen: { id: DisplayId } }
+	| { area: { screen: DisplayId; bounds: LogicalBounds } };
 export type CursorAnimationStyle = "regular" | "slow" | "fast";
 export type CursorConfiguration = {
 	hide?: boolean;
@@ -550,7 +556,21 @@ export type GeneralSettingsStore = {
 	enableNewRecordingFlow: boolean;
 	postDeletionBehaviour?: PostDeletionBehaviour;
 };
-export type GifExportSettings = { fps: number; resolution_base: XY<number> };
+export type GifExportSettings = {
+	fps: number;
+	resolution_base: XY<number>;
+	quality: GifQuality | null;
+};
+export type GifQuality = {
+	/**
+	 * Encoding quality from 1-100 (default: 90)
+	 */
+	quality: number | null;
+	/**
+	 * Whether to prioritize speed over quality (default: false)
+	 */
+	fast: boolean | null;
+};
 export type HapticPattern = "Alignment" | "LevelChange" | "Generic";
 export type HapticPerformanceTime = "Default" | "Now" | "DrawCompleted";
 export type Hotkey = {
@@ -568,6 +588,8 @@ export type HotkeysConfiguration = { show: boolean };
 export type HotkeysStore = { hotkeys: { [key in HotkeyAction]: Hotkey } };
 export type InstantRecordingMeta = { fps: number; sample_rate: number | null };
 export type JsonValue<T> = [T];
+export type LayoutMode = "default" | "cameraOnly" | "hideCamera";
+export type LayoutSegment = { start: number; end: number; mode?: LayoutMode };
 export type LogicalBounds = { position: LogicalPosition; size: LogicalSize };
 export type LogicalPosition = { x: number; y: number };
 export type LogicalSize = { width: number; height: number };
@@ -661,9 +683,9 @@ export type RequestOpenSettings = { page: string };
 export type RequestStartRecording = null;
 export type S3UploadMeta = { id: string };
 export type ScreenCaptureTarget =
-	| { variant: "window"; id: number }
-	| { variant: "screen"; id: number }
-	| { variant: "area"; screen: number; bounds: Bounds };
+	| { variant: "window"; id: WindowId }
+	| { variant: "display"; id: DisplayId }
+	| { variant: "area"; screen: DisplayId; bounds: LogicalBounds };
 export type ScreenUnderCursor = {
 	name: string;
 	physical_size: PhysicalSize;
@@ -694,9 +716,9 @@ export type ShowCapWindow =
 	| { Settings: { page: string | null } }
 	| { Editor: { project_path: string } }
 	| "RecordingsOverlay"
-	| { WindowCaptureOccluder: { screen_id: number } }
+	| { WindowCaptureOccluder: { screen_id: DisplayId } }
 	| { TargetSelectOverlay: { display_id: DisplayId } }
-	| { CaptureArea: { screen_id: number } }
+	| { CaptureArea: { screen_id: DisplayId } }
 	| "Camera"
 	| { InProgressRecording: { countdown: number | null } }
 	| "Upgrade"
@@ -721,12 +743,6 @@ export type TargetUnderCursor = {
 	window: WindowUnderCursor | null;
 	screen: ScreenUnderCursor | null;
 };
-export type LayoutSegment = {
-	start: number;
-	end: number;
-	mode?: "default" | "cameraOnly" | "hideCamera";
-};
-
 export type TimelineConfiguration = {
 	segments: TimelineSegment[];
 	zoomSegments: ZoomSegment[];
diff --git a/biome.json b/biome.json
index 6ef5792715..550497650c 100644
--- a/biome.json
+++ b/biome.json
@@ -30,5 +30,13 @@
 				"organizeImports": "on"
 			}
 		}
-	}
+	},
+	"overrides": [
+		{
+			"includes": ["apps/desktop/**/*"],
+			"linter": {
+				"rules": { "a11y": "off" }
+			}
+		}
+	]
 }
diff --git a/crates/audio/src/audio_data.rs b/crates/audio/src/audio_data.rs
index fc2a013ff0..587d62cd75 100644
--- a/crates/audio/src/audio_data.rs
+++ b/crates/audio/src/audio_data.rs
@@ -4,6 +4,8 @@ use ffmpeg::{
 };
 use std::path::Path;
 
+use crate::cast_bytes_to_f32_slice;
+
 // F32 Packed 48kHz audio
 pub struct AudioData {
     samples: Vec<f32>,
@@ -169,9 +171,3 @@ impl AudioData {
         self.samples.len() / self.channels as usize
     }
 }
-
-/// # Safety
-/// This function assumes that the input slice is aligned to a 4-byte boundary.
-pub unsafe fn cast_bytes_to_f32_slice(slice: &[u8]) -> &[f32] {
-    unsafe { std::slice::from_raw_parts(slice.as_ptr() as *const f32, slice.len() / 4) }
-}
diff --git a/crates/audio/src/lib.rs b/crates/audio/src/lib.rs
index 4d989ff500..a0df1c4c62 100644
--- a/crates/audio/src/lib.rs
+++ b/crates/audio/src/lib.rs
@@ -3,3 +3,39 @@ mod renderer;
 
 pub use audio_data::*;
 pub use renderer::*;
+
+pub trait FromSampleBytes: cpal::SizedSample + std::fmt::Debug + Send + 'static {
+    const BYTE_SIZE: usize;
+
+    fn from_bytes(bytes: &[u8]) -> Self;
+}
+
+macro_rules! sample_bytes {
+    ( $( $num:ty, $size:literal ),* ) => (
+        $(
+            impl FromSampleBytes for $num {
+                const BYTE_SIZE: usize = $size;
+
+                fn from_bytes(bytes: &[u8]) -> Self {
+                    Self::from_le_bytes(bytes.try_into().expect("Incorrect byte slice length"))
+                }
+            }
+        )*
+    )
+}
+
+sample_bytes!(u8, 1, i16, 2, i32, 4, i64, 8, f32, 4, f64, 8);
+
+/// # Safety
+/// The input slice must be aligned to the size of `f32`.
+pub unsafe fn cast_f32_slice_to_bytes(slice: &[f32]) -> &[u8] {
+    unsafe { std::slice::from_raw_parts(slice.as_ptr() as *const u8, slice.len() * f32::BYTE_SIZE) }
+}
+
+/// # Safety
+/// The input slice must be aligned to the size of `f32`.
+pub unsafe fn cast_bytes_to_f32_slice(slice: &[u8]) -> &[f32] {
+    unsafe {
+        std::slice::from_raw_parts(slice.as_ptr() as *const f32, slice.len() / f32::BYTE_SIZE)
+    }
+}
diff --git a/crates/audio/src/main.rs b/crates/audio/src/main.rs
index a9b7966d64..2c76259c1b 100644
--- a/crates/audio/src/main.rs
+++ b/crates/audio/src/main.rs
@@ -1,22 +1,10 @@
-use cap_audio::AudioData;
+use cap_audio::{AudioData, cast_f32_slice_to_bytes};
 use ffmpeg::{
     codec as avcodec,
     format::{self as avformat},
 };
 use std::path::Path;
 
-/// # Safety
-/// This function assumes that the input slice contains valid f32 values and that the length of the slice is a multiple of 4.
-pub unsafe fn cast_f32_slice_to_bytes(slice: &[f32]) -> &[u8] {
-    unsafe { std::slice::from_raw_parts(slice.as_ptr() as *const u8, slice.len() * 4) }
-}
-
-/// # Safety
-/// This function assumes that the input slice contains valid f32 values and that the length of the slice is a multiple of 4.
-pub unsafe fn cast_bytes_to_f32_slice(slice: &[u8]) -> &[f32] {
-    unsafe { std::slice::from_raw_parts(slice.as_ptr() as *const f32, slice.len() / 4) }
-}
-
 pub fn opus_encode_audio(path: impl AsRef<Path>, samples: &AudioData) {
     let mut output = avformat::output(&path).unwrap();
 
diff --git a/crates/audio/src/playback.rs b/crates/audio/src/playback.rs
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/crates/camera-avfoundation/Cargo.toml b/crates/camera-avfoundation/Cargo.toml
index ec24935a40..4eba06649e 100644
--- a/crates/camera-avfoundation/Cargo.toml
+++ b/crates/camera-avfoundation/Cargo.toml
@@ -2,6 +2,7 @@
 name = "cap-camera-avfoundation"
 version = "0.1.0"
 edition = "2024"
+license = "MIT"
 
 [target.'cfg(target_os = "macos")'.dependencies]
 cidre = { workspace = true }
diff --git a/crates/camera-avfoundation/examples/cli.rs b/crates/camera-avfoundation/examples/cli.rs
index 4d9aab2af6..95014b5a22 100644
--- a/crates/camera-avfoundation/examples/cli.rs
+++ b/crates/camera-avfoundation/examples/cli.rs
@@ -53,8 +53,8 @@ pub fn main() {
         for fr_range in fr_ranges.iter() {
             _formats.push(Format {
                 index: i,
-                width: desc.dimensions().width,
-                height: desc.dimensions().height,
+                width: desc.dims().width,
+                height: desc.dims().height,
                 fourcc: desc.media_sub_type(),
                 color_space,
                 max_frame_rate: (
diff --git a/crates/camera-directshow/Cargo.toml b/crates/camera-directshow/Cargo.toml
index f4f668855f..b9d780b070 100644
--- a/crates/camera-directshow/Cargo.toml
+++ b/crates/camera-directshow/Cargo.toml
@@ -2,6 +2,7 @@
 name = "cap-camera-directshow"
 version = "0.1.0"
 edition = "2024"
+license = "MIT"
 
 [dependencies]
 tracing.workspace = true
@@ -26,15 +27,3 @@ workspace = true
 inquire = "0.7.5"
 tracing.workspace = true
 tracing-subscriber = "0.3.19"
-
-[target.'cfg(windows)'.dev-dependencies]
-windows-core = { workspace = true }
-windows = { workspace = true, features = [
-	"Win32_System_Com",
-	"Win32_Media_MediaFoundation",
-	"Win32_Media_DirectShow",
-	"Win32_System_Com_StructuredStorage",
-	"Win32_System_Ole",
-	"Win32_System_Variant",
-	"Win32_Media_KernelStreaming",
-] }
diff --git a/crates/camera-directshow/examples/cli.rs b/crates/camera-directshow/examples/cli.rs
index af19899f03..bad9b5761b 100644
--- a/crates/camera-directshow/examples/cli.rs
+++ b/crates/camera-directshow/examples/cli.rs
@@ -1,6 +1,6 @@
 fn main() {
     #[cfg(windows)]
-    windows::run();
+    windows::main();
     #[cfg(not(windows))]
     panic!("This example is only available on Windows");
 }
@@ -19,7 +19,7 @@ mod windows {
         core::Interface,
     };
 
-    fn main() {
+    pub fn main() {
         tracing_subscriber::fmt::init();
 
         unsafe {
diff --git a/crates/camera-ffmpeg/Cargo.toml b/crates/camera-ffmpeg/Cargo.toml
index 88abdbfb05..c924c9ffb9 100644
--- a/crates/camera-ffmpeg/Cargo.toml
+++ b/crates/camera-ffmpeg/Cargo.toml
@@ -2,6 +2,7 @@
 name = "cap-camera-ffmpeg"
 version = "0.1.0"
 edition = "2024"
+license = "MIT"
 
 [dependencies]
 ffmpeg = { workspace = true }
diff --git a/crates/camera-mediafoundation/Cargo.toml b/crates/camera-mediafoundation/Cargo.toml
index b8281db9ac..3bc52bb58b 100644
--- a/crates/camera-mediafoundation/Cargo.toml
+++ b/crates/camera-mediafoundation/Cargo.toml
@@ -2,6 +2,7 @@
 name = "cap-camera-mediafoundation"
 version = "0.1.0"
 edition = "2024"
+license = "MIT"
 
 [dependencies]
 tracing.workspace = true
diff --git a/crates/camera-mediafoundation/examples/cli.rs b/crates/camera-mediafoundation/examples/cli.rs
index 2916d4eb94..317e15f65c 100644
--- a/crates/camera-mediafoundation/examples/cli.rs
+++ b/crates/camera-mediafoundation/examples/cli.rs
@@ -1,6 +1,6 @@
 fn main() {
     #[cfg(windows)]
-    windows::run();
+    windows::main();
     #[cfg(not(windows))]
     panic!("This example is only available on Windows");
 }
diff --git a/crates/camera-windows/Cargo.toml b/crates/camera-windows/Cargo.toml
index befc53dbe2..4960cfcab4 100644
--- a/crates/camera-windows/Cargo.toml
+++ b/crates/camera-windows/Cargo.toml
@@ -2,6 +2,7 @@
 name = "cap-camera-windows"
 version = "0.1.0"
 edition = "2024"
+license = "MIT"
 
 [dependencies]
 thiserror.workspace = true
@@ -19,9 +20,3 @@ workspace = true
 
 [dev-dependencies]
 inquire = "0.7.5"
-
-cap-camera-mediafoundation = { path = "../camera-mediafoundation" }
-cap-camera-directshow = { path = "../camera-directshow" }
-
-windows = { workspace = true }
-windows-core = { workspace = true }
diff --git a/crates/camera-windows/examples/cli.rs b/crates/camera-windows/examples/cli.rs
index 7c4c598a41..cf73abca5c 100644
--- a/crates/camera-windows/examples/cli.rs
+++ b/crates/camera-windows/examples/cli.rs
@@ -1,6 +1,6 @@
 fn main() {
     #[cfg(windows)]
-    windows::run();
+    windows::main();
     #[cfg(not(windows))]
     panic!("This example is only available on Windows");
 }
@@ -11,7 +11,7 @@ mod windows {
 
     use cap_camera_windows::*;
 
-    fn run() {
+    pub fn main() {
         let devices = get_devices()
             .unwrap()
             .into_iter()
diff --git a/crates/camera/Cargo.toml b/crates/camera/Cargo.toml
index aa63af062a..ff64fcd156 100644
--- a/crates/camera/Cargo.toml
+++ b/crates/camera/Cargo.toml
@@ -2,9 +2,10 @@
 name = "cap-camera"
 version = "0.1.0"
 edition = "2024"
+license = "MIT"
 
 [dependencies]
-thiserror.workspace = true
+thiserror = { workspace = true }
 serde = { workspace = true, optional = true }
 specta = { workspace = true, optional = true }
 
diff --git a/crates/camera/src/macos.rs b/crates/camera/src/macos.rs
index 389c3b02f6..715a8f5784 100644
--- a/crates/camera/src/macos.rs
+++ b/crates/camera/src/macos.rs
@@ -25,8 +25,8 @@ impl CameraInfo {
 
         for format in device.formats().iter() {
             let desc = format.format_desc();
-            let width = desc.dimensions().width as u32;
-            let height = desc.dimensions().height as u32;
+            let width = desc.dims().width as u32;
+            let height = desc.dims().height as u32;
 
             for fr_range in format.video_supported_frame_rate_ranges().iter() {
                 // SAFETY: trust me bro it crashes on intel mac otherwise
diff --git a/crates/cursor-capture/src/main.rs b/crates/cursor-capture/src/main.rs
index 4715f2115e..9131818717 100644
--- a/crates/cursor-capture/src/main.rs
+++ b/crates/cursor-capture/src/main.rs
@@ -4,9 +4,9 @@ use cap_displays::Display;
 fn main() {
     loop {
         let position = RawCursorPosition::get()
-            .relative_to_display(Display::list()[0])
-            .normalize()
-            .with_crop((0.0, 0.0), (1.0, 1.0));
+            .relative_to_display(Display::list()[1])
+            .unwrap()
+            .normalize();
 
         println!("{position:?}");
     }
diff --git a/crates/cursor-capture/src/position.rs b/crates/cursor-capture/src/position.rs
index 7b071fe38a..b41248784c 100644
--- a/crates/cursor-capture/src/position.rs
+++ b/crates/cursor-capture/src/position.rs
@@ -1,10 +1,11 @@
-use cap_displays::Display;
+use cap_displays::{Display, bounds::*};
 use device_query::{DeviceQuery, DeviceState};
 
+// Physical on Windows, Logical on macOS
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub struct RawCursorPosition {
-    pub(crate) x: i32,
-    pub(crate) y: i32,
+    x: i32,
+    y: i32,
 }
 
 impl RawCursorPosition {
@@ -18,7 +19,7 @@ impl RawCursorPosition {
         }
     }
 
-    pub fn relative_to_display(&self, display: Display) -> RelativeCursorPosition {
+    pub fn relative_to_display(&self, display: Display) -> Option<RelativeCursorPosition> {
         RelativeCursorPosition::from_raw(*self, display)
     }
 }
@@ -26,74 +27,75 @@ impl RawCursorPosition {
 // relative to display using top-left origin
 #[derive(Clone, Copy)]
 pub struct RelativeCursorPosition {
-    pub(crate) x: i32,
-    pub(crate) y: i32,
-    pub(crate) display: Display,
+    x: i32,
+    y: i32,
+    display: Display,
 }
 
 impl RelativeCursorPosition {
-    pub fn from_raw(raw: RawCursorPosition, display: Display) -> Self {
-        #[cfg(target_os = "macos")]
+    pub fn from_raw(raw: RawCursorPosition, display: Display) -> Option<Self> {
+        #[cfg(windows)]
         {
-            let raw_display = display.raw_handle().inner();
-            let display_bounds = raw_display.bounds();
+            let physical_bounds = display.raw_handle().physical_bounds()?;
 
-            Self {
-                x: raw.x - display_bounds.origin.x as i32,
-                y: raw.y - display_bounds.origin.y as i32,
+            return Some(Self {
+                x: raw.x - physical_bounds.position().x() as i32,
+                y: raw.y - physical_bounds.position().y() as i32,
                 display,
-            }
+            });
         }
 
-        #[cfg(windows)]
+        #[cfg(target_os = "macos")]
         {
-            let _ = (raw, display);
-            todo!()
-        }
-    }
+            let logical_bounds = display.raw_handle().logical_bounds()?;
 
-    pub fn x(&self) -> i32 {
-        self.x
-    }
-
-    pub fn y(&self) -> i32 {
-        self.y
+            return Some(Self {
+                x: raw.x - logical_bounds.position().x() as i32,
+                y: raw.y - logical_bounds.position().y() as i32,
+                display,
+            });
+        }
     }
 
     pub fn display(&self) -> &Display {
         &self.display
     }
 
-    pub fn normalize(&self) -> NormalizedCursorPosition {
-        #[cfg(target_os = "macos")]
-        let (x, y) = {
-            let display_bounds = self.display().raw_handle().inner().bounds();
-
-            (
-                self.x as f32 / display_bounds.size.width as f32,
-                self.y as f32 / display_bounds.size.height as f32,
-            )
-        };
-
+    pub fn normalize(&self) -> Option<NormalizedCursorPosition> {
         #[cfg(windows)]
-        #[allow(unused_variables)]
-        let (x, y) = {
-            todo!();
-            // let display_bounds = self.display().raw_handle().bounds();
-
-            // (
-            //     self.x as f32 / (display_bounds.right - display_bounds.left) as f32,
-            //     self.y as f32 / (display_bounds.bottom - display_bounds.top) as f32,
-            // )
-        };
+        {
+            let bounds = self.display().raw_handle().physical_bounds()?;
+            let size = bounds.size();
+
+            Some(NormalizedCursorPosition {
+                x: self.x as f64 / size.width(),
+                y: self.y as f64 / size.height(),
+                crop: CursorCropBounds {
+                    x: 0.0,
+                    y: 0.0,
+                    width: size.width(),
+                    height: size.height(),
+                },
+                display: self.display,
+            })
+        }
 
-        #[allow(unreachable_code)]
-        NormalizedCursorPosition {
-            x,
-            y,
-            crop_position: (0.0, 0.0),
-            crop_size: (1.0, 1.0),
-            display: self.display,
+        #[cfg(target_os = "macos")]
+        {
+            let bounds = self.display().raw_handle().logical_bounds()?;
+            let size = bounds.size();
+
+            Some(NormalizedCursorPosition {
+                x: self.x as f64 / size.width(),
+                y: self.y as f64 / size.height(),
+                crop: CursorCropBounds {
+                    x: 0.0,
+                    y: 0.0,
+                    width: size.width(),
+                    height: size.height(),
+                },
+                display: self.display,
+            })
         }
     }
 }
@@ -107,20 +109,67 @@ impl std::fmt::Debug for RelativeCursorPosition {
     }
 }
 
+#[derive(Clone, Copy, Debug)]
+/// Needs to be logical coordinates on macOS and physical on Windows
+/// This type is opqaue on purpose as the logical/physical invariants need to hold
+pub struct CursorCropBounds {
+    x: f64,
+    y: f64,
+    width: f64,
+    height: f64,
+}
+
+impl CursorCropBounds {
+    #[cfg(target_os = "macos")]
+    pub fn new_macos(bounds: LogicalBounds) -> Self {
+        Self {
+            x: bounds.position().x(),
+            y: bounds.position().y(),
+            width: bounds.size().width(),
+            height: bounds.size().height(),
+        }
+    }
+
+    #[cfg(target_os = "windows")]
+    pub fn new_windows(bounds: PhysicalBounds) -> Self {
+        Self {
+            x: bounds.position().x(),
+            y: bounds.position().y(),
+            width: bounds.size().width(),
+            height: bounds.size().height(),
+        }
+    }
+
+    pub fn x(&self) -> f64 {
+        self.x
+    }
+
+    pub fn y(&self) -> f64 {
+        self.y
+    }
+
+    pub fn width(&self) -> f64 {
+        self.width
+    }
+
+    pub fn height(&self) -> f64 {
+        self.height
+    }
+}
+
 pub struct NormalizedCursorPosition {
-    pub(crate) x: f32,
-    pub(crate) y: f32,
-    pub(crate) crop_position: (f32, f32),
-    pub(crate) crop_size: (f32, f32),
-    pub(crate) display: Display,
+    x: f64,
+    y: f64,
+    crop: CursorCropBounds,
+    display: Display,
 }
 
 impl NormalizedCursorPosition {
-    pub fn x(&self) -> f32 {
+    pub fn x(&self) -> f64 {
         self.x
     }
 
-    pub fn y(&self) -> f32 {
+    pub fn y(&self) -> f64 {
         self.y
     }
 
@@ -128,25 +177,20 @@ impl NormalizedCursorPosition {
         &self.display
     }
 
-    pub fn crop_position(&self) -> (f32, f32) {
-        self.crop_position
-    }
-
-    pub fn crop_size(&self) -> (f32, f32) {
-        self.crop_size
+    pub fn crop(&self) -> CursorCropBounds {
+        self.crop
     }
 
-    pub fn with_crop(&self, position: (f32, f32), size: (f32, f32)) -> Self {
+    pub fn with_crop(&self, crop: CursorCropBounds) -> Self {
         let raw_px = (
-            self.x * self.crop_size.0 + self.crop_position.0,
-            self.y * self.crop_size.1 + self.crop_position.1,
+            self.x * self.crop.width + self.crop.x,
+            self.y * self.crop.height + self.crop.y,
         );
 
         Self {
-            x: (raw_px.0 - position.0) / size.0,
-            y: (raw_px.1 - position.1) / size.1,
-            crop_position: position,
-            crop_size: size,
+            x: (raw_px.0 - crop.x) / crop.width,
+            y: (raw_px.1 - crop.y) / crop.height,
+            crop,
             display: self.display,
         }
     }
@@ -177,19 +221,22 @@ mod tests {
         let original_normalized = NormalizedCursorPosition {
             x: 0.5,
             y: 0.5,
-            crop_position: (0.0, 0.0),
-            crop_size: (1.0, 1.0),
+            crop_position: LogicalPosition::new(0.0, 0.0),
+            crop_size: LogicalSize::new(1.0, 1.0),
             display,
         };
 
-        let cropped_position = (0.0, 0.0);
-        let cropped_size = (1.0, 1.0);
+        let cropped_position = LogicalPosition::new(0.0, 0.0);
+        let cropped_size = LogicalSize::new(1.0, 1.0);
         let new_normalized = original_normalized.with_crop(cropped_position, cropped_size);
 
         assert_eq!(new_normalized.x, 0.5);
         assert_eq!(new_normalized.y, 0.5);
-        assert_eq!(new_normalized.crop_position(), (0.0, 0.0));
-        assert_eq!(new_normalized.crop_size(), (1.0, 1.0));
+        assert_eq!(
+            new_normalized.crop_position(),
+            LogicalPosition::new(0.0, 0.0)
+        );
+        assert_eq!(new_normalized.crop_size(), LogicalSize::new(1.0, 1.0));
     }
 
     #[test]
@@ -198,13 +245,13 @@ mod tests {
         let original_normalized = NormalizedCursorPosition {
             x: 0.5,
             y: 0.5,
-            crop_position: (0.0, 0.0),
-            crop_size: (1.0, 1.0),
+            crop_position: LogicalPosition::new(0.0, 0.0),
+            crop_size: LogicalSize::new(1.0, 1.0),
             display,
         };
 
-        let cropped_position = (0.25, 0.25);
-        let cropped_size = (0.5, 0.5);
+        let cropped_position = LogicalPosition::new(0.25, 0.25);
+        let cropped_size = LogicalSize::new(0.5, 0.5);
         let new_normalized = original_normalized.with_crop(cropped_position, cropped_size);
 
         // Original point (0.5, 0.5) is in the center of the (0,0) to (1,1) range.
@@ -213,24 +260,24 @@ mod tests {
         let expected_x = (0.5 * 1.0 + 0.0 - 0.25) / 0.5;
         let expected_y = (0.5 * 1.0 + 0.0 - 0.25) / 0.5;
 
-        assert!((new_normalized.x - expected_x).abs() < f32::EPSILON);
-        assert!((new_normalized.y - expected_y).abs() < f32::EPSILON);
-        assert_eq!(new_normalized.crop_position(), (0.25, 0.25));
-        assert_eq!(new_normalized.crop_size(), (0.5, 0.5));
+        assert!((new_normalized.x - expected_x).abs() < f64::EPSILON);
+        assert!((new_normalized.y - expected_y).abs() < f64::EPSILON);
+        assert_eq!(new_normalized.crop_position(), cropped_position);
+        assert_eq!(new_normalized.crop_size(), cropped_size);
     }
 
     #[test]
     fn test_with_crop_top_left_of_crop() {
         let display = mock_display();
 
-        let cropped_position = (0.25, 0.25);
-        let cropped_size = (0.5, 0.5);
+        let cropped_position = LogicalPosition::new(0.25, 0.25);
+        let cropped_size = LogicalSize::new(0.5, 0.5);
 
         let original_normalized_at_crop_tl = NormalizedCursorPosition {
             x: 0.25,
             y: 0.25,
-            crop_position: (0.0, 0.0),
-            crop_size: (1.0, 1.0),
+            crop_position: LogicalPosition::new(0.0, 0.0),
+            crop_size: LogicalSize::new(1.0, 1.0),
             display,
         };
 
@@ -239,24 +286,24 @@ mod tests {
 
         // The point that was at the top-left of the crop in the original space
         // should now be at (0.0, 0.0) in the new cropped space.
-        assert!((new_normalized.x - 0.0).abs() < f32::EPSILON);
-        assert!((new_normalized.y - 0.0).abs() < f32::EPSILON);
-        assert_eq!(new_normalized.crop_position(), (0.25, 0.25));
-        assert_eq!(new_normalized.crop_size(), (0.5, 0.5));
+        assert!((new_normalized.x - 0.0).abs() < f64::EPSILON);
+        assert!((new_normalized.y - 0.0).abs() < f64::EPSILON);
+        assert_eq!(new_normalized.crop_position(), cropped_position);
+        assert_eq!(new_normalized.crop_size(), cropped_size);
     }
 
     #[test]
     fn test_with_crop_bottom_right_of_crop() {
         let display = mock_display();
 
-        let cropped_position = (0.25, 0.25);
-        let cropped_size = (0.5, 0.5);
+        let cropped_position = LogicalPosition::new(0.25, 0.25);
+        let cropped_size = LogicalSize::new(0.5, 0.5);
 
         let original_normalized_at_crop_br = NormalizedCursorPosition {
             x: 0.75,
             y: 0.75,
-            crop_position: (0.0, 0.0),
-            crop_size: (1.0, 1.0),
+            crop_position: LogicalPosition::new(0.0, 0.0),
+            crop_size: LogicalSize::new(1.0, 1.0),
             display,
         };
 
@@ -265,10 +312,10 @@ mod tests {
 
         // The point that was at the bottom-right of the crop in the original space
         // should now be at (1.0, 1.0) in the new cropped space.
-        assert!((new_normalized.x - 1.0).abs() < f32::EPSILON);
-        assert!((new_normalized.y - 1.0).abs() < f32::EPSILON);
-        assert_eq!(new_normalized.crop_position(), (0.25, 0.25));
-        assert_eq!(new_normalized.crop_size(), (0.5, 0.5));
+        assert!((new_normalized.x - 1.0).abs() < f64::EPSILON);
+        assert!((new_normalized.y - 1.0).abs() < f64::EPSILON);
+        assert_eq!(new_normalized.crop_position(), cropped_position);
+        assert_eq!(new_normalized.crop_size(), cropped_size);
     }
 
     #[test]
@@ -277,8 +324,8 @@ mod tests {
         let original_normalized = NormalizedCursorPosition {
             x: 0.5, // This 0.5 is within the first crop
             y: 0.5, // This 0.5 is within the first crop
-            crop_position: (0.1, 0.1),
-            crop_size: (0.8, 0.8),
+            crop_position: LogicalPosition::new(0.1, 0.1),
+            crop_size: LogicalSize::new(0.8, 0.8),
             display,
         };
 
@@ -286,8 +333,8 @@ mod tests {
         // Raw x = 0.5 * 0.8 + 0.1 = 0.4 + 0.1 = 0.5
         // Raw y = 0.5 * 0.8 + 0.1 = 0.4 + 0.1 = 0.5
 
-        let second_crop_position = (0.2, 0.2);
-        let second_crop_size = (0.6, 0.6);
+        let second_crop_position = LogicalPosition::new(0.2, 0.2);
+        let second_crop_size = LogicalSize::new(0.6, 0.6);
 
         // The second crop is from 0.2 to 0.8 in the original space.
         // The raw position is (0.5, 0.5).
@@ -297,9 +344,12 @@ mod tests {
 
         let new_normalized = original_normalized.with_crop(second_crop_position, second_crop_size);
 
-        assert!((new_normalized.x - 0.5).abs() < f32::EPSILON);
-        assert!((new_normalized.y - 0.5).abs() < f32::EPSILON);
-        assert_eq!(new_normalized.crop_position(), (0.2, 0.2));
-        assert_eq!(new_normalized.crop_size(), (0.6, 0.6));
+        assert!((new_normalized.x - 0.5).abs() < f64::EPSILON);
+        assert!((new_normalized.y - 0.5).abs() < f64::EPSILON);
+        assert_eq!(
+            new_normalized.crop_position(),
+            LogicalPosition::new(0.2, 0.2)
+        );
+        assert_eq!(new_normalized.crop_size(), LogicalSize::new(0.6, 0.6));
     }
 }
diff --git a/crates/displays/Cargo.toml b/crates/displays/Cargo.toml
index 8f4ffd5a8a..18070abb9e 100644
--- a/crates/displays/Cargo.toml
+++ b/crates/displays/Cargo.toml
@@ -10,8 +10,10 @@ workspace = true
 serde = { version = "1.0.219", features = ["derive"] }
 specta.workspace = true
 image = "0.24"
+tracing.workspace = true
 
 [target.'cfg(target_os = "macos")'.dependencies]
+cidre = { workspace = true, default-features = false, features = ["sc"] }
 core-graphics = "0.24.0"
 core-foundation = "0.10.0"
 cocoa = "0.26.0"
@@ -19,17 +21,19 @@ objc = "0.2.7"
 
 [target.'cfg(target_os= "windows")'.dependencies]
 windows = { workspace = true, features = [
-    "Win32_Foundation",
-    "Win32_System",
-    "Win32_System_Threading",
-    "Win32_System_Registry",
-    "Win32_System_Com",
-    "Win32_System_Wmi",
-    "Win32_UI_WindowsAndMessaging",
-    "Win32_UI_Shell",
-    "Win32_UI_HiDpi",
-    "Win32_Graphics_Gdi",
-    "Win32_Storage_FileSystem",
-    "Win32_Devices_Display",
+	"Win32_Foundation",
+	"Win32_System",
+	"Win32_System_Threading",
+	"Win32_System_Registry",
+	"Win32_System_Com",
+	"Win32_System_Wmi",
+	"Win32_UI_WindowsAndMessaging",
+	"Win32_UI_Shell",
+	"Win32_UI_HiDpi",
+	"Win32_Graphics_Dwm",
+	"Win32_Graphics_Gdi",
+	"Win32_Storage_FileSystem",
+	"Win32_Devices_Display",
+	"Graphics_Capture",
+	"Win32_System_WinRT_Graphics_Capture",
 ] }
-windows-sys = { workspace = true }
diff --git a/crates/displays/src/bounds.rs b/crates/displays/src/bounds.rs
index 724ba59e16..cc34ad47c3 100644
--- a/crates/displays/src/bounds.rs
+++ b/crates/displays/src/bounds.rs
@@ -1,7 +1,7 @@
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
 use specta::Type;
 
-#[derive(Clone, Copy, Debug, Type, Serialize)]
+#[derive(Clone, Copy, Debug, Type, Serialize, Deserialize)]
 pub struct LogicalBounds {
     pub(crate) position: LogicalPosition,
     pub(crate) size: LogicalSize,
@@ -28,13 +28,44 @@ impl LogicalBounds {
     }
 }
 
-#[derive(Clone, Copy, Debug, Type, Serialize)]
+#[derive(Clone, Copy, Debug, Type, Serialize, Deserialize)]
+pub struct PhysicalBounds {
+    pub(crate) position: PhysicalPosition,
+    pub(crate) size: PhysicalSize,
+}
+
+impl PhysicalBounds {
+    pub fn new(position: PhysicalPosition, size: PhysicalSize) -> Self {
+        Self { position, size }
+    }
+
+    pub fn position(&self) -> PhysicalPosition {
+        self.position
+    }
+
+    pub fn size(&self) -> PhysicalSize {
+        self.size
+    }
+
+    pub fn contains_point(&self, point: PhysicalPosition) -> bool {
+        point.x() >= self.position.x()
+            && point.x() < self.position.x() + self.size.width()
+            && point.y() >= self.position.y()
+            && point.y() < self.position.y() + self.size.height()
+    }
+}
+
+#[derive(Clone, Copy, Debug, Type, Serialize, Deserialize, PartialEq)]
 pub struct LogicalSize {
     pub(crate) width: f64,
     pub(crate) height: f64,
 }
 
 impl LogicalSize {
+    pub fn new(width: f64, height: f64) -> Self {
+        Self { width, height }
+    }
+
     pub fn width(&self) -> f64 {
         self.width
     }
@@ -44,7 +75,7 @@ impl LogicalSize {
     }
 }
 
-#[derive(Clone, Copy, Debug, Type, Serialize)]
+#[derive(Clone, Copy, Debug, Type, Serialize, Deserialize, PartialEq)]
 pub struct PhysicalSize {
     pub(crate) width: f64,
     pub(crate) height: f64,
@@ -62,15 +93,26 @@ impl PhysicalSize {
     pub fn height(&self) -> f64 {
         self.height
     }
+
+    pub fn map(&self, f: impl Fn(f64) -> f64) -> Self {
+        Self {
+            width: f(self.width),
+            height: f(self.height),
+        }
+    }
 }
 
-#[derive(Clone, Copy, Debug, Type, Serialize)]
+#[derive(Clone, Copy, Debug, Type, Serialize, Deserialize, PartialEq)]
 pub struct LogicalPosition {
     pub(crate) x: f64,
     pub(crate) y: f64,
 }
 
 impl LogicalPosition {
+    pub fn new(x: f64, y: f64) -> Self {
+        Self { x, y }
+    }
+
     pub fn x(&self) -> f64 {
         self.x
     }
@@ -80,7 +122,7 @@ impl LogicalPosition {
     }
 }
 
-#[derive(Clone, Copy, Debug, Type, Serialize)]
+#[derive(Clone, Copy, Debug, Type, Serialize, Deserialize)]
 pub struct PhysicalPosition {
     pub(crate) x: f64,
     pub(crate) y: f64,
diff --git a/crates/displays/src/lib.rs b/crates/displays/src/lib.rs
index 9ad6b69d26..be57e60736 100644
--- a/crates/displays/src/lib.rs
+++ b/crates/displays/src/lib.rs
@@ -1,12 +1,13 @@
 pub mod bounds;
 pub mod platform;
 
-use std::str::FromStr;
-
-use bounds::{LogicalBounds, PhysicalSize};
+use bounds::*;
 pub use platform::{DisplayIdImpl, DisplayImpl, WindowIdImpl, WindowImpl};
 use serde::{Deserialize, Serialize};
 use specta::Type;
+use std::str::FromStr;
+
+use crate::bounds::{LogicalPosition, LogicalSize};
 
 #[derive(Clone, Copy)]
 pub struct Display(DisplayImpl);
@@ -16,6 +17,10 @@ impl Display {
         DisplayImpl::list().into_iter().map(Self).collect()
     }
 
+    pub fn primary() -> Self {
+        Self(DisplayImpl::primary())
+    }
+
     pub fn raw_handle(&self) -> &DisplayImpl {
         &self.0
     }
@@ -24,28 +29,32 @@ impl Display {
         DisplayId(self.0.raw_id())
     }
 
-    pub fn from_id(id: DisplayId) -> Option<Self> {
-        Self::list().into_iter().find(|d| d.id() == id)
+    pub fn from_id(id: &DisplayId) -> Option<Self> {
+        Self::list().into_iter().find(|d| &d.id() == id)
     }
 
     pub fn get_containing_cursor() -> Option<Self> {
         DisplayImpl::get_containing_cursor().map(Self)
     }
 
-    pub fn name(&self) -> String {
+    pub fn name(&self) -> Option<String> {
         self.0.name()
     }
 
-    pub fn physical_size(&self) -> PhysicalSize {
+    pub fn physical_size(&self) -> Option<PhysicalSize> {
         self.0.physical_size()
     }
 
+    pub fn logical_size(&self) -> Option<LogicalSize> {
+        self.0.logical_size()
+    }
+
     pub fn refresh_rate(&self) -> f64 {
         self.0.refresh_rate()
     }
 }
 
-#[derive(Serialize, Deserialize, Type, Clone, PartialEq)]
+#[derive(Serialize, Deserialize, Type, Clone, PartialEq, Debug)]
 pub struct DisplayId(
     #[serde(with = "serde_display_id")]
     #[specta(type = String)]
@@ -110,8 +119,16 @@ impl Window {
         WindowId(self.0.id())
     }
 
-    pub fn bounds(&self) -> Option<LogicalBounds> {
-        self.0.bounds()
+    pub fn from_id(id: &WindowId) -> Option<Self> {
+        Self::list().into_iter().find(|d| &d.id() == id)
+    }
+
+    pub fn physical_size(&self) -> Option<PhysicalSize> {
+        self.0.physical_size()
+    }
+
+    pub fn logical_size(&self) -> Option<LogicalSize> {
+        self.0.logical_size()
     }
 
     pub fn owner_name(&self) -> Option<String> {
@@ -125,9 +142,63 @@ impl Window {
     pub fn raw_handle(&self) -> &WindowImpl {
         &self.0
     }
+
+    pub fn display(&self) -> Option<Display> {
+        self.0.display().map(Display)
+    }
+
+    pub fn name(&self) -> Option<String> {
+        self.0.name()
+    }
+
+    pub fn display_relative_logical_bounds(&self) -> Option<LogicalBounds> {
+        let display = self.display()?;
+
+        #[cfg(target_os = "macos")]
+        {
+            let display_logical_bounds = display.raw_handle().logical_bounds()?;
+            let window_logical_bounds = self.raw_handle().logical_bounds()?;
+
+            Some(LogicalBounds::new(
+                LogicalPosition::new(
+                    window_logical_bounds.position().x() - display_logical_bounds.position().x(),
+                    window_logical_bounds.position().y() - display_logical_bounds.position().y(),
+                ),
+                window_logical_bounds.size(),
+            ))
+        }
+
+        #[cfg(windows)]
+        {
+            let display_physical_bounds = display.raw_handle().physical_bounds()?;
+            let display_logical_size = display.logical_size()?;
+            let window_physical_bounds: PhysicalBounds = self.raw_handle().physical_bounds()?;
+
+            let scale = display_logical_size.width() / display_physical_bounds.size().width;
+
+            let display_relative_physical_bounds = PhysicalBounds::new(
+                PhysicalPosition::new(
+                    window_physical_bounds.position().x - display_physical_bounds.position().x,
+                    window_physical_bounds.position().y - display_physical_bounds.position().y,
+                ),
+                window_physical_bounds.size(),
+            );
+
+            Some(LogicalBounds::new(
+                LogicalPosition::new(
+                    display_relative_physical_bounds.position().x() * scale,
+                    display_relative_physical_bounds.position().y() * scale,
+                ),
+                LogicalSize::new(
+                    display_relative_physical_bounds.size().width() * scale,
+                    display_relative_physical_bounds.size().height() * scale,
+                ),
+            ))
+        }
+    }
 }
 
-#[derive(Serialize, Deserialize, Type, Clone, PartialEq)]
+#[derive(Serialize, Deserialize, Type, Clone, PartialEq, Debug)]
 pub struct WindowId(
     #[serde(with = "serde_window_id")]
     #[specta(type = String)]
diff --git a/crates/displays/src/main.rs b/crates/displays/src/main.rs
index fd498328d4..0a5ff9da16 100644
--- a/crates/displays/src/main.rs
+++ b/crates/displays/src/main.rs
@@ -1,14 +1,21 @@
 use std::time::Duration;
 
 fn main() {
+    #[cfg(windows)]
+    {
+        use windows::Win32::UI::HiDpi::{PROCESS_PER_MONITOR_DPI_AWARE, SetProcessDpiAwareness};
+
+        unsafe { SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE).unwrap() };
+    }
+
     // Test display functionality
     println!("=== Display Information ===");
     for (index, display) in cap_displays::Display::list().iter().enumerate() {
-        println!("Display {}: {}", index + 1, display.name());
+        println!("Display {}: {}", index + 1, display.name().unwrap());
         println!("  ID: {}", display.id());
 
-        let logical_size = display.raw_handle().logical_size();
-        let physical_size = display.physical_size();
+        let logical_size = display.raw_handle().logical_size().unwrap();
+        let physical_size = display.physical_size().unwrap();
         let refresh_rate = display.refresh_rate();
 
         println!(
@@ -45,7 +52,10 @@ fn main() {
     }
 
     if let Some(cursor_display) = cap_displays::Display::get_containing_cursor() {
-        println!("🖱️  Cursor is currently on: {}", cursor_display.name());
+        println!(
+            "🖱️  Cursor is currently on: {}",
+            cursor_display.name().unwrap()
+        );
         println!();
     }
 
@@ -61,7 +71,8 @@ fn main() {
             // Limit to first 5 windows
             println!("\nWindow {}: {}", index + 1, window.id());
 
-            if let Some(bounds) = window.bounds() {
+            #[cfg(target_os = "macos")]
+            if let Some(bounds) = window.raw_handle().logical_bounds() {
                 println!(
                     "  Bounds: {}x{} at ({}, {})",
                     bounds.size().width(),
diff --git a/crates/displays/src/platform/macos.rs b/crates/displays/src/platform/macos.rs
index c83d5e92b3..8aee9123a4 100644
--- a/crates/displays/src/platform/macos.rs
+++ b/crates/displays/src/platform/macos.rs
@@ -1,12 +1,17 @@
 use std::{ffi::c_void, str::FromStr};
 
-use core_foundation::{base::FromVoid, number::CFNumber, string::CFString};
+use cidre::{arc, ns, sc};
+use cocoa::appkit::NSScreen;
+use core_foundation::{array::CFArray, base::FromVoid, number::CFNumber, string::CFString};
 use core_graphics::{
     display::{
         CFDictionary, CGDirectDisplayID, CGDisplay, CGDisplayBounds, CGDisplayCopyDisplayMode,
         CGRect, kCGWindowListOptionIncludingWindow,
     },
-    window::{CGWindowID, kCGWindowBounds, kCGWindowLayer, kCGWindowNumber, kCGWindowOwnerName},
+    window::{
+        CGWindowID, kCGWindowBounds, kCGWindowLayer, kCGWindowName, kCGWindowNumber,
+        kCGWindowOwnerName,
+    },
 };
 
 use crate::bounds::{LogicalBounds, LogicalPosition, LogicalSize, PhysicalSize};
@@ -40,13 +45,13 @@ impl DisplayImpl {
         Self::list().into_iter().find(|d| d.0.id == parsed_id)
     }
 
-    pub fn logical_size(&self) -> LogicalSize {
+    pub fn logical_size(&self) -> Option<LogicalSize> {
         let rect = unsafe { CGDisplayBounds(self.0.id) };
 
-        LogicalSize {
+        Some(LogicalSize {
             width: rect.size.width,
             height: rect.size.height,
-        }
+        })
     }
 
     pub fn logical_position(&self) -> LogicalPosition {
@@ -58,36 +63,48 @@ impl DisplayImpl {
         }
     }
 
+    pub fn logical_bounds(&self) -> Option<LogicalBounds> {
+        Some(LogicalBounds {
+            position: self.logical_position(),
+            size: self.logical_size()?,
+        })
+    }
+
     pub fn get_containing_cursor() -> Option<Self> {
         let cursor = get_cursor_position()?;
 
         Self::list().into_iter().find(|display| {
+            let Some(logical_size) = display.logical_size() else {
+                return false;
+            };
+
             let bounds = LogicalBounds {
                 position: display.logical_position(),
-                size: display.logical_size(),
+                size: logical_size,
             };
             bounds.contains_point(cursor)
         })
     }
 
-    pub fn physical_size(&self) -> PhysicalSize {
+    pub fn physical_size(&self) -> Option<PhysicalSize> {
         let mode = unsafe { CGDisplayCopyDisplayMode(self.0.id) };
         if mode.is_null() {
-            return PhysicalSize {
-                width: 0.0,
-                height: 0.0,
-            };
+            return None;
         }
 
-        let width = unsafe { core_graphics::display::CGDisplayModeGetWidth(mode) };
-        let height = unsafe { core_graphics::display::CGDisplayModeGetHeight(mode) };
+        let width = unsafe { core_graphics::display::CGDisplayModeGetPixelWidth(mode) };
+        let height = unsafe { core_graphics::display::CGDisplayModeGetPixelHeight(mode) };
 
         unsafe { core_graphics::display::CGDisplayModeRelease(mode) };
 
-        PhysicalSize {
+        Some(PhysicalSize {
             width: width as f64,
             height: height as f64,
-        }
+        })
+    }
+
+    pub fn scale(&self) -> Option<f64> {
+        Some(unsafe { NSScreen::backingScaleFactor(self.as_ns_screen()?) })
     }
 
     pub fn refresh_rate(&self) -> f64 {
@@ -103,12 +120,32 @@ impl DisplayImpl {
         refresh_rate
     }
 
-    pub fn name(&self) -> String {
+    pub fn name(&self) -> Option<String> {
+        use cocoa::base::id;
+        use cocoa::foundation::NSString;
+        use objc::{msg_send, *};
+        use std::ffi::CStr;
+
+        unsafe {
+            if let Some(ns_screen) = self.as_ns_screen() {
+                let name: id = msg_send![ns_screen, localizedName];
+                if !name.is_null() {
+                    let name = CStr::from_ptr(NSString::UTF8String(name))
+                        .to_string_lossy()
+                        .to_string();
+                    return Some(name);
+                }
+            }
+        }
+
+        None
+    }
+
+    fn as_ns_screen(&self) -> Option<*mut objc::runtime::Object> {
         use cocoa::appkit::NSScreen;
         use cocoa::base::{id, nil};
         use cocoa::foundation::{NSArray, NSDictionary, NSString};
         use objc::{msg_send, *};
-        use std::ffi::CStr;
 
         unsafe {
             let screens = NSScreen::screens(nil);
@@ -126,19 +163,41 @@ impl DisplayImpl {
                 let num_value: u32 = msg_send![num, unsignedIntValue];
 
                 if num_value == self.0.id {
-                    let name: id = msg_send![screen, localizedName];
-                    if !name.is_null() {
-                        let name = CStr::from_ptr(NSString::UTF8String(name))
-                            .to_string_lossy()
-                            .to_string();
-                        return name;
-                    }
+                    return Some(screen);
                 }
             }
-
-            // Fallback to generic name with display ID
-            format!("Display {}", self.0.id)
         }
+
+        None
+    }
+}
+
+impl DisplayImpl {
+    pub async fn as_sc(&self) -> Option<arc::R<sc::Display>> {
+        sc::ShareableContent::current()
+            .await
+            .ok()?
+            .displays()
+            .iter()
+            .find(|d| d.display_id().0 == self.0.id)
+            .map(|v| v.retained())
+    }
+
+    pub async fn as_content_filter(&self) -> Option<arc::R<sc::ContentFilter>> {
+        self.as_content_filter_excluding_windows(vec![]).await
+    }
+
+    pub async fn as_content_filter_excluding_windows(
+        &self,
+        windows: Vec<arc::R<sc::Window>>,
+    ) -> Option<arc::R<sc::ContentFilter>> {
+        let excluded_windows =
+            ns::Array::from_slice_retained(windows.into_iter().collect::<Vec<_>>().as_slice());
+
+        Some(sc::ContentFilter::with_display_excluding_windows(
+            self.as_sc().await?.as_ref(),
+            &excluded_windows,
+        ))
     }
 }
 
@@ -203,7 +262,7 @@ impl WindowImpl {
         Self::list()
             .into_iter()
             .filter_map(|window| {
-                let bounds = window.bounds()?;
+                let bounds = window.logical_bounds()?;
                 bounds.contains_point(cursor).then_some(window)
             })
             .collect()
@@ -258,7 +317,21 @@ impl WindowImpl {
         }
     }
 
-    pub fn bounds(&self) -> Option<LogicalBounds> {
+    pub fn name(&self) -> Option<String> {
+        let windows =
+            core_graphics::window::copy_window_info(kCGWindowListOptionIncludingWindow, self.0)?;
+
+        let window_dict =
+            unsafe { CFDictionary::<CFString, *const c_void>::from_void(*windows.get(0)?) };
+
+        unsafe {
+            window_dict
+                .find(kCGWindowName)
+                .map(|v| CFString::from_void(*v).to_string())
+        }
+    }
+
+    pub fn logical_bounds(&self) -> Option<LogicalBounds> {
         let windows =
             core_graphics::window::copy_window_info(kCGWindowListOptionIncludingWindow, self.0)?;
 
@@ -282,6 +355,22 @@ impl WindowImpl {
         })
     }
 
+    pub fn logical_size(&self) -> Option<LogicalSize> {
+        Some(self.logical_bounds()?.size())
+    }
+
+    pub fn physical_size(&self) -> Option<PhysicalSize> {
+        let logical_bounds = self.logical_bounds()?;
+        let display = self.display()?;
+
+        let scale = display.physical_size()?.width() / display.logical_size()?.width();
+
+        Some(PhysicalSize {
+            width: logical_bounds.size().width() * scale,
+            height: logical_bounds.size().height() * scale,
+        })
+    }
+
     pub fn app_icon(&self) -> Option<Vec<u8>> {
         use cocoa::base::{id, nil};
         use cocoa::foundation::{NSArray, NSAutoreleasePool, NSString};
@@ -358,6 +447,38 @@ impl WindowImpl {
             result
         }
     }
+
+    pub async fn as_sc(&self) -> Option<arc::R<sc::Window>> {
+        sc::ShareableContent::current()
+            .await
+            .ok()?
+            .windows()
+            .iter()
+            .find(|w| w.id() == self.0)
+            .map(|v| v.retained())
+    }
+
+    pub fn display(&self) -> Option<DisplayImpl> {
+        let descriptions = core_graphics::window::create_description_from_array(
+            CFArray::from_copyable(&[self.0]),
+        )?;
+
+        let window_bounds = CGRect::from_dict_representation(
+            &descriptions
+                .get(0)?
+                .get(unsafe { kCGWindowBounds })
+                .downcast::<CFDictionary>()?,
+        )?;
+
+        for id in CGDisplay::active_displays().ok()? {
+            let display = CGDisplay::new(id);
+            if window_bounds.is_intersects(&display.bounds()) {
+                return Some(DisplayImpl(display));
+            }
+        }
+
+        None
+    }
 }
 
 #[derive(Clone, PartialEq, Debug)]
diff --git a/crates/displays/src/platform/win.rs b/crates/displays/src/platform/win.rs
index a09ea60a39..697ed47072 100644
--- a/crates/displays/src/platform/win.rs
+++ b/crates/displays/src/platform/win.rs
@@ -1,6 +1,7 @@
-use std::{mem, str::FromStr};
-
+use std::{ffi::OsString, mem, os::windows::ffi::OsStringExt, path::PathBuf, str::FromStr};
+use tracing::error;
 use windows::{
+    Graphics::Capture::GraphicsCaptureItem,
     Win32::{
         Devices::Display::{
             DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME,
@@ -11,13 +12,17 @@ use windows::{
             QueryDisplayConfig,
         },
         Foundation::{CloseHandle, HWND, LPARAM, POINT, RECT, TRUE, WIN32_ERROR, WPARAM},
-        Graphics::Gdi::{
-            BI_RGB, BITMAP, BITMAPINFO, BITMAPINFOHEADER, CreateCompatibleBitmap,
-            CreateCompatibleDC, CreateSolidBrush, DEVMODEW, DIB_RGB_COLORS, DeleteDC, DeleteObject,
-            ENUM_CURRENT_SETTINGS, EnumDisplayMonitors, EnumDisplaySettingsW, FillRect, GetDC,
-            GetDIBits, GetMonitorInfoW, GetObjectA, HBRUSH, HDC, HGDIOBJ, HMONITOR,
-            MONITOR_DEFAULTTONEAREST, MONITOR_DEFAULTTONULL, MONITORINFOEXW, MonitorFromPoint,
-            ReleaseDC, SelectObject,
+        Graphics::{
+            Dwm::{DWMWA_CLOAKED, DWMWA_EXTENDED_FRAME_BOUNDS, DwmGetWindowAttribute},
+            Gdi::{
+                BI_RGB, BITMAP, BITMAPINFO, BITMAPINFOHEADER, CreateCompatibleBitmap,
+                CreateCompatibleDC, CreateSolidBrush, DEVMODEW, DIB_RGB_COLORS,
+                DISPLAY_DEVICE_STATE_FLAGS, DISPLAY_DEVICEW, DeleteDC, DeleteObject,
+                ENUM_CURRENT_SETTINGS, EnumDisplayDevicesW, EnumDisplayMonitors,
+                EnumDisplaySettingsW, FillRect, GetDC, GetDIBits, GetMonitorInfoW, GetObjectA,
+                HBRUSH, HDC, HGDIOBJ, HMONITOR, MONITOR_DEFAULTTONEAREST, MONITOR_DEFAULTTONULL,
+                MONITORINFOEXW, MonitorFromPoint, MonitorFromWindow, ReleaseDC, SelectObject,
+            },
         },
         Storage::FileSystem::{GetFileVersionInfoSizeW, GetFileVersionInfoW, VerQueryValueW},
         System::{
@@ -29,27 +34,40 @@ use windows::{
                 GetCurrentProcessId, OpenProcess, PROCESS_NAME_FORMAT,
                 PROCESS_QUERY_LIMITED_INFORMATION, QueryFullProcessImageNameW,
             },
+            WinRT::Graphics::Capture::IGraphicsCaptureItemInterop,
         },
         UI::{
-            HiDpi::GetDpiForWindow,
+            HiDpi::{
+                GetDpiForMonitor, GetDpiForWindow, GetProcessDpiAwareness, MDT_EFFECTIVE_DPI,
+                PROCESS_PER_MONITOR_DPI_AWARE,
+            },
             Shell::ExtractIconExW,
             WindowsAndMessaging::{
-                DI_FLAGS, DestroyIcon, DrawIconEx, EnumWindows, GCLP_HICON, GW_HWNDNEXT,
-                GWL_EXSTYLE, GetClassLongPtrW, GetClassNameW, GetCursorPos, GetIconInfo,
-                GetLayeredWindowAttributes, GetWindow, GetWindowLongW, GetWindowRect,
-                GetWindowThreadProcessId, HICON, ICONINFO, IsIconic, IsWindowVisible, SendMessageW,
-                WM_GETICON, WS_EX_LAYERED, WS_EX_TOPMOST, WS_EX_TRANSPARENT, WindowFromPoint,
+                DI_FLAGS, DestroyIcon, DrawIconEx, EnumChildWindows, EnumWindows, GCLP_HICON,
+                GW_HWNDNEXT, GWL_EXSTYLE, GWL_STYLE, GetClassLongPtrW, GetClassNameW,
+                GetClientRect, GetCursorPos, GetDesktopWindow, GetIconInfo,
+                GetLayeredWindowAttributes, GetWindow, GetWindowLongPtrW, GetWindowLongW,
+                GetWindowRect, GetWindowTextLengthW, GetWindowTextW, GetWindowThreadProcessId,
+                HICON, ICONINFO, IsIconic, IsWindowVisible, SendMessageW, WM_GETICON, WS_CHILD,
+                WS_EX_LAYERED, WS_EX_TOOLWINDOW, WS_EX_TOPMOST, WS_EX_TRANSPARENT, WindowFromPoint,
             },
         },
     },
     core::{BOOL, PCWSTR, PWSTR},
 };
 
-use crate::bounds::{LogicalBounds, LogicalPosition, LogicalSize, PhysicalSize};
+use crate::bounds::{LogicalSize, PhysicalBounds, PhysicalPosition, PhysicalSize};
+
+// All of this assumes PROCESS_PER_MONITOR_DPI_AWARE
+//
+// On Windows it's nigh impossible to get the logical position of a display
+// or window, since there's no simple API that accounts for each monitor having different DPI.
 
 #[derive(Clone, Copy)]
 pub struct DisplayImpl(HMONITOR);
 
+unsafe impl Send for DisplayImpl {}
+
 impl DisplayImpl {
     pub fn primary() -> Self {
         // Find the primary monitor by checking the MONITORINFOF_PRIMARY flag
@@ -112,41 +130,21 @@ impl DisplayImpl {
         Self::list().into_iter().find(|d| d.raw_id().0 == parsed_id)
     }
 
-    pub fn logical_size(&self) -> LogicalSize {
-        let mut info = MONITORINFOEXW::default();
-        info.monitorInfo.cbSize = mem::size_of::<MONITORINFOEXW>() as u32;
+    pub fn logical_size(&self) -> Option<LogicalSize> {
+        let physical_size = self.physical_size()?;
 
-        unsafe {
-            if GetMonitorInfoW(self.0, &mut info as *mut _ as *mut _).as_bool() {
-                let rect = info.monitorInfo.rcMonitor;
-                LogicalSize {
-                    width: (rect.right - rect.left) as f64,
-                    height: (rect.bottom - rect.top) as f64,
-                }
-            } else {
-                LogicalSize {
-                    width: 0.0,
-                    height: 0.0,
-                }
-            }
-        }
-    }
+        let dpi = unsafe {
+            let mut dpi_x = 0;
+            GetDpiForMonitor(self.0, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut 0).ok()?;
+            dpi_x
+        };
 
-    pub fn logical_position(&self) -> LogicalPosition {
-        let mut info = MONITORINFOEXW::default();
-        info.monitorInfo.cbSize = mem::size_of::<MONITORINFOEXW>() as u32;
+        let scale = dpi as f64 / 96.0;
 
-        unsafe {
-            if GetMonitorInfoW(self.0, &mut info as *mut _ as *mut _).as_bool() {
-                let rect = info.monitorInfo.rcMonitor;
-                LogicalPosition {
-                    x: rect.left as f64,
-                    y: rect.top as f64,
-                }
-            } else {
-                LogicalPosition { x: 0.0, y: 0.0 }
-            }
-        }
+        Some(LogicalSize::new(
+            physical_size.width() / scale,
+            physical_size.height() / scale,
+        ))
     }
 
     pub fn get_containing_cursor() -> Option<Self> {
@@ -164,40 +162,30 @@ impl DisplayImpl {
         }
     }
 
-    pub fn physical_size(&self) -> PhysicalSize {
+    pub fn physical_bounds(&self) -> Option<PhysicalBounds> {
         let mut info = MONITORINFOEXW::default();
         info.monitorInfo.cbSize = mem::size_of::<MONITORINFOEXW>() as u32;
 
-        unsafe {
-            if GetMonitorInfoW(self.0, &mut info as *mut _ as *mut _).as_bool() {
-                let device_name = info.szDevice;
-                let mut devmode = DEVMODEW::default();
-                devmode.dmSize = mem::size_of::<DEVMODEW>() as u16;
-
-                if EnumDisplaySettingsW(
-                    PCWSTR(device_name.as_ptr()),
-                    ENUM_CURRENT_SETTINGS,
-                    &mut devmode,
+        unsafe { GetMonitorInfoW(self.0, &mut info as *mut _ as *mut _) }
+            .as_bool()
+            .then(|| {
+                let rect = info.monitorInfo.rcMonitor;
+                PhysicalBounds::new(
+                    PhysicalPosition::new(rect.left as f64, rect.top as f64),
+                    PhysicalSize::new(
+                        rect.right as f64 - rect.left as f64,
+                        rect.bottom as f64 - rect.top as f64,
+                    ),
                 )
-                .as_bool()
-                {
-                    PhysicalSize {
-                        width: devmode.dmPelsWidth as f64,
-                        height: devmode.dmPelsHeight as f64,
-                    }
-                } else {
-                    PhysicalSize {
-                        width: 0.0,
-                        height: 0.0,
-                    }
-                }
-            } else {
-                PhysicalSize {
-                    width: 0.0,
-                    height: 0.0,
-                }
-            }
-        }
+            })
+    }
+
+    pub fn physical_position(&self) -> Option<PhysicalPosition> {
+        Some(self.physical_bounds()?.position())
+    }
+
+    pub fn physical_size(&self) -> Option<PhysicalSize> {
+        Some(self.physical_bounds()?.size())
     }
 
     pub fn refresh_rate(&self) -> f64 {
@@ -227,48 +215,43 @@ impl DisplayImpl {
         }
     }
 
-    pub fn name(&self) -> String {
-        // First try the modern DisplayConfig API for friendly names
-        if let Some(friendly_name) = self.get_friendly_name_from_displayconfig() {
-            return friendly_name;
-        }
-
-        // Try WMI query for better localized names
-        if let Some(wmi_name) = self.get_friendly_name_from_wmi() {
-            return wmi_name;
-        }
-
-        // Fallback to the existing registry method
-        let mut info = MONITORINFOEXW::default();
-        info.monitorInfo.cbSize = mem::size_of::<MONITORINFOEXW>() as u32;
-
+    pub fn name(&self) -> Option<String> {
         unsafe {
-            if GetMonitorInfoW(self.0, &mut info as *mut _ as *mut _).as_bool() {
-                // Convert the device name from wide string to String
-                let device_name = &info.szDevice;
-                let null_pos = device_name
-                    .iter()
-                    .position(|&c| c == 0)
-                    .unwrap_or(device_name.len());
-                let device_name_str = String::from_utf16_lossy(&device_name[..null_pos]);
-
-                // Try to get friendly name from registry
-                if let Some(friendly_name) = self.get_friendly_name_from_registry(&device_name_str)
-                {
-                    return friendly_name;
-                }
+            let mut monitor_info = MONITORINFOEXW {
+                monitorInfo: windows::Win32::Graphics::Gdi::MONITORINFO {
+                    cbSize: mem::size_of::<MONITORINFOEXW>() as u32,
+                    rcMonitor: RECT::default(),
+                    rcWork: RECT::default(),
+                    dwFlags: 0,
+                },
+                szDevice: [0; 32],
+            };
+
+            if GetMonitorInfoW(self.0, &mut monitor_info as *mut _ as *mut _).as_bool() {
+                let device_name = PCWSTR::from_raw(monitor_info.szDevice.as_ptr());
+
+                let mut display_device = DISPLAY_DEVICEW {
+                    cb: mem::size_of::<DISPLAY_DEVICEW>() as u32,
+                    DeviceName: [0; 32],
+                    DeviceString: [0; 128],
+                    StateFlags: DISPLAY_DEVICE_STATE_FLAGS(0),
+                    DeviceID: [0; 128],
+                    DeviceKey: [0; 128],
+                };
 
-                // Try EDID-based name lookup as final fallback before device name
-                if let Some(edid_name) = self.get_friendly_name_from_edid(&device_name_str) {
-                    return edid_name;
-                }
+                if EnumDisplayDevicesW(device_name, 0, &mut display_device, 0).as_bool() {
+                    let device_string = display_device.DeviceString;
+                    let len = device_string
+                        .iter()
+                        .position(|&x| x == 0)
+                        .unwrap_or(device_string.len());
 
-                // Final fallback to device name
-                device_name_str
-            } else {
-                format!("Unknown Display")
+                    return Some(String::from_utf16_lossy(&device_string[..len]));
+                }
             }
         }
+
+        None
     }
 
     fn get_friendly_name_from_displayconfig(&self) -> Option<String> {
@@ -649,13 +632,18 @@ impl DisplayImpl {
         }
         None
     }
+
+    pub fn try_as_capture_item(&self) -> windows::core::Result<GraphicsCaptureItem> {
+        let interop = windows::core::factory::<GraphicsCaptureItem, IGraphicsCaptureItemInterop>()?;
+        unsafe { interop.CreateForMonitor(self.0) }
+    }
 }
 
-fn get_cursor_position() -> Option<LogicalPosition> {
+fn get_cursor_position() -> Option<PhysicalPosition> {
     let mut point = POINT { x: 0, y: 0 };
     unsafe {
         if GetCursorPos(&mut point).is_ok() {
-            Some(LogicalPosition {
+            Some(PhysicalPosition {
                 x: point.x as f64,
                 y: point.y as f64,
             })
@@ -691,7 +679,8 @@ impl WindowImpl {
         };
 
         unsafe {
-            let _ = EnumWindows(
+            let _ = EnumChildWindows(
+                Some(GetDesktopWindow()),
                 Some(enum_windows_proc),
                 LPARAM(std::ptr::addr_of_mut!(context) as isize),
             );
@@ -700,6 +689,10 @@ impl WindowImpl {
         context.list
     }
 
+    pub fn inner(&self) -> HWND {
+        self.0
+    }
+
     pub fn get_topmost_at_cursor() -> Option<Self> {
         let cursor = get_cursor_position()?;
         let point = POINT {
@@ -799,7 +792,7 @@ impl WindowImpl {
         Self::list()
             .into_iter()
             .filter_map(|window| {
-                let bounds = window.bounds()?;
+                let bounds = window.physical_bounds()?;
                 bounds.contains_point(cursor).then_some(window)
             })
             .collect()
@@ -1217,33 +1210,183 @@ impl WindowImpl {
         }
     }
 
-    pub fn bounds(&self) -> Option<LogicalBounds> {
+    pub fn logical_size(&self) -> Option<LogicalSize> {
         let mut rect = RECT::default();
+
         unsafe {
-            if GetWindowRect(self.0, &mut rect).is_ok() {
-                // Get DPI scaling factor to convert physical to logical coordinates
-                const BASE_DPI: f64 = 96.0;
-                let dpi = match GetDpiForWindow(self.0) {
-                    0 => BASE_DPI as u32,
-                    dpi => dpi,
-                } as f64;
-                let scale_factor = dpi / BASE_DPI;
-
-                Some(LogicalBounds {
-                    position: LogicalPosition {
-                        x: rect.left as f64 / scale_factor,
-                        y: rect.top as f64 / scale_factor,
-                    },
-                    size: LogicalSize {
-                        width: (rect.right - rect.left) as f64 / scale_factor,
-                        height: (rect.bottom - rect.top) as f64 / scale_factor,
-                    },
-                })
-            } else {
-                None
+            match GetProcessDpiAwareness(None) {
+                Ok(PROCESS_PER_MONITOR_DPI_AWARE) => {}
+                Err(e) => {
+                    error!("Failed to get process DPI awareness: {e}");
+                    return None;
+                }
+                Ok(v) => {
+                    error!("Unsupported DPI awareness {v:?}");
+                    return None;
+                }
             }
+
+            DwmGetWindowAttribute(
+                self.0,
+                DWMWA_EXTENDED_FRAME_BOUNDS,
+                (&raw mut rect).cast(),
+                size_of::<RECT>() as u32,
+            )
+            .ok()?;
+
+            const BASE_DPI: f64 = 96.0;
+            let dpi = match GetDpiForWindow(self.0) {
+                0 => BASE_DPI as u32,
+                dpi => dpi,
+            } as f64;
+            let scale_factor = dpi / BASE_DPI;
+
+            Some(LogicalSize {
+                width: (rect.right - rect.left) as f64 / scale_factor,
+                height: (rect.bottom - rect.top) as f64 / scale_factor,
+            })
         }
     }
+
+    pub fn physical_bounds(&self) -> Option<PhysicalBounds> {
+        let mut rect = RECT::default();
+        unsafe {
+            match GetProcessDpiAwareness(None) {
+                Ok(PROCESS_PER_MONITOR_DPI_AWARE) => {}
+                Err(e) => {
+                    error!("Failed to get process DPI awareness: {e}");
+                    return None;
+                }
+                Ok(v) => {
+                    error!("Unsupported DPI awareness {v:?}");
+                    return None;
+                }
+            }
+
+            DwmGetWindowAttribute(
+                self.0,
+                DWMWA_EXTENDED_FRAME_BOUNDS,
+                (&raw mut rect).cast(),
+                size_of::<RECT>() as u32,
+            )
+            .ok()?;
+
+            Some(PhysicalBounds {
+                position: PhysicalPosition {
+                    x: rect.left as f64,
+                    y: rect.top as f64,
+                },
+                size: PhysicalSize {
+                    width: (rect.right - rect.left) as f64,
+                    height: (rect.bottom - rect.top) as f64,
+                },
+            })
+        }
+    }
+
+    pub fn physical_size(&self) -> Option<PhysicalSize> {
+        Some(self.physical_bounds()?.size())
+    }
+
+    pub fn physical_position(&self) -> Option<PhysicalPosition> {
+        Some(self.physical_bounds()?.position())
+    }
+
+    pub fn display(&self) -> Option<DisplayImpl> {
+        let hwmonitor = unsafe { MonitorFromWindow(self.0, MONITOR_DEFAULTTONULL) };
+        if hwmonitor.is_invalid() {
+            None
+        } else {
+            Some(DisplayImpl(hwmonitor))
+        }
+    }
+
+    pub fn name(&self) -> Option<String> {
+        let len = unsafe { GetWindowTextLengthW(self.0) };
+
+        let mut name = vec![0u16; usize::try_from(len).unwrap() + 1];
+        if len >= 1 {
+            let copied = unsafe { GetWindowTextW(self.0, &mut name) };
+            if copied == 0 {
+                return Some(String::new());
+            }
+        }
+
+        String::from_utf16(
+            &name
+                .as_slice()
+                .iter()
+                .take_while(|ch| **ch != 0x0000)
+                .copied()
+                .collect::<Vec<u16>>(),
+        )
+        .ok()
+    }
+
+    pub fn is_on_screen(&self) -> bool {
+        if !unsafe { IsWindowVisible(self.0) }.as_bool() {
+            return false;
+        }
+
+        let mut pvattribute_cloaked = 0u32;
+        unsafe {
+            DwmGetWindowAttribute(
+                self.0,
+                DWMWA_CLOAKED,
+                &mut pvattribute_cloaked as *mut _ as *mut std::ffi::c_void,
+                std::mem::size_of::<u32>() as u32,
+            )
+        }
+        .ok();
+
+        if pvattribute_cloaked != 0 {
+            return false;
+        }
+
+        let mut process_id = 0;
+        unsafe { GetWindowThreadProcessId(self.0, Some(&mut process_id)) };
+
+        let owner_process_path = match unsafe { pid_to_exe_path(process_id) } {
+            Ok(path) => path,
+            Err(_) => return false,
+        };
+
+        if owner_process_path.starts_with("C:\\Windows\\SystemApps") {
+            return false;
+        }
+
+        true
+    }
+
+    pub fn is_valid(&self) -> bool {
+        if !unsafe { IsWindowVisible(self.0).as_bool() } {
+            return false;
+        }
+
+        let mut id = 0;
+        unsafe { GetWindowThreadProcessId(self.0, Some(&mut id)) };
+        if id == unsafe { GetCurrentProcessId() } {
+            return false;
+        }
+
+        let mut rect = RECT::default();
+        let result = unsafe { GetClientRect(self.0, &mut rect) };
+        if result.is_ok() {
+            let styles = unsafe { GetWindowLongPtrW(self.0, GWL_STYLE) };
+            let ex_styles = unsafe { GetWindowLongPtrW(self.0, GWL_EXSTYLE) };
+
+            if (ex_styles & isize::try_from(WS_EX_TOOLWINDOW.0).unwrap()) != 0 {
+                return false;
+            }
+            if (styles & isize::try_from(WS_CHILD.0).unwrap()) != 0 {
+                return false;
+            }
+        } else {
+            return false;
+        }
+
+        true
+    }
 }
 
 fn is_window_valid_for_enumeration(hwnd: HWND, current_process_id: u32) -> bool {
@@ -1375,3 +1518,26 @@ impl FromStr for WindowIdImpl {
             .map_err(|_| "Invalid window ID".to_string())
     }
 }
+
+unsafe fn pid_to_exe_path(pid: u32) -> Result<PathBuf, windows::core::Error> {
+    let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) }?;
+    if handle.is_invalid() {
+        tracing::error!("Invalid PID {}", pid);
+    }
+    let mut lpexename = [0u16; 1024];
+    let mut lpdwsize = lpexename.len() as u32;
+
+    let query = unsafe {
+        QueryFullProcessImageNameW(
+            handle,
+            PROCESS_NAME_FORMAT::default(),
+            windows::core::PWSTR(lpexename.as_mut_ptr()),
+            &mut lpdwsize,
+        )
+    };
+    unsafe { CloseHandle(handle) }.ok();
+    query?;
+
+    let os_str = &OsString::from_wide(&lpexename[..lpdwsize as usize]);
+    Ok(PathBuf::from(os_str))
+}
diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml
index fce8acc4a7..575f3892cf 100644
--- a/crates/editor/Cargo.toml
+++ b/crates/editor/Cargo.toml
@@ -24,3 +24,4 @@ futures = { workspace = true }
 tracing.workspace = true
 flume.workspace = true
 tokio-util = "0.7.15"
+ringbuf = "0.4.8"
diff --git a/crates/editor/examples/cli.rs b/crates/editor/examples/cli.rs
index 1488046811..cda67fcbe2 100644
--- a/crates/editor/examples/cli.rs
+++ b/crates/editor/examples/cli.rs
@@ -6,7 +6,9 @@ use std::path::PathBuf;
 async fn main() {
     let path: PathBuf = std::env::args().collect::<Vec<_>>().swap_remove(1).into();
 
-    let editor = EditorInstance::new(path, |_s| {}).await.unwrap();
+    let editor = EditorInstance::new(path, |_s| {}, Box::new(|_| {}))
+        .await
+        .unwrap();
 
     editor.start_playback(30, XY::new(1920, 1080)).await;
 
diff --git a/crates/media/src/feeds/audio.rs b/crates/editor/src/audio.rs
similarity index 76%
rename from crates/media/src/feeds/audio.rs
rename to crates/editor/src/audio.rs
index d9e9a03da9..3edfec4567 100644
--- a/crates/media/src/feeds/audio.rs
+++ b/crates/editor/src/audio.rs
@@ -1,115 +1,14 @@
-use cap_audio::{AudioData, StereoMode};
+use cap_audio::{AudioData, FromSampleBytes, StereoMode, cast_f32_slice_to_bytes};
+use cap_media::MediaError;
 use cap_media_info::AudioInfo;
 use cap_project::{AudioConfiguration, ProjectConfiguration, TimelineConfiguration};
-use ffmpeg::{ChannelLayout, format as avformat, software::resampling};
+use ffmpeg::{ChannelLayout, format as avformat, frame::Audio as FFAudio, software::resampling};
 use ringbuf::{
     HeapRb,
     traits::{Consumer, Observer, Producer},
 };
 use std::sync::Arc;
 
-use crate::{
-    MediaError,
-    data::{FFAudio, FromSampleBytes, cast_f32_slice_to_bytes},
-};
-
-// fn decode_audio_to_f32(
-//     decoder: &mut decoder::Audio,
-//     input_ctx: &mut format::context::Input,
-//     stream_index: usize,
-// ) -> Vec<f32> {
-//     let mut decoded_frame = ffmpeg::frame::Audio::empty();
-//     let mut resampled_frame = ffmpeg::frame::Audio::empty();
-
-//     let mut resampler = ffmpeg::software::resampler(
-//         (decoder.format(), decoder.channel_layout(), decoder.rate()),
-//         (
-//             Sample::F32(Type::Packed),
-//             decoder.channel_layout(),
-//             AudioData::SAMPLE_RATE,
-//         ),
-//     )
-//     .unwrap();
-
-//     // let mut resampled_frames = 0;
-//     let mut samples: Vec<f32> = vec![];
-
-//     fn process_resampler(
-//         resampler: &mut resampling::Context,
-//         samples: &mut Vec<f32>,
-//         resampled_frame: &mut FFAudio,
-//     ) {
-//         loop {
-//             let resample_delay = resampler.flush(resampled_frame).unwrap();
-//             if resampled_frame.samples() == 0 {
-//                 break;
-//             }
-
-//             let slice = &resampled_frame.data(0)
-//                 [0..resampled_frame.samples() * 4 * resampled_frame.channels() as usize];
-//             samples.extend(unsafe { cast_bytes_to_f32_slice(slice) });
-
-//             if resample_delay.is_none() {
-//                 break;
-//             }
-//         }
-//     }
-
-//     fn process_decoder(
-//         decoder: &mut ffmpeg::decoder::Audio,
-//         decoded_frame: &mut FFAudio,
-//         resampler: &mut resampling::Context,
-//         resampled_frame: &mut FFAudio,
-//         samples: &mut Vec<f32>,
-//     ) {
-//         while decoder.receive_frame(decoded_frame).is_ok() {
-//             let resample_delay = resampler.run(decoded_frame, resampled_frame).unwrap();
-
-//             let slice = &resampled_frame.data(0)
-//                 [0..resampled_frame.samples() * 4 * resampled_frame.channels() as usize];
-//             samples.extend(unsafe { cast_bytes_to_f32_slice(slice) });
-
-//             if resample_delay.is_some() {
-//                 process_resampler(resampler, samples, resampled_frame);
-//             }
-//         }
-//     }
-
-//     for (stream, packet) in input_ctx.packets() {
-//         if stream.index() != stream_index {
-//             continue;
-//         }
-
-//         decoder.send_packet(&packet).unwrap();
-
-//         process_decoder(
-//             decoder,
-//             &mut decoded_frame,
-//             &mut resampler,
-//             &mut resampled_frame,
-//             &mut samples,
-//         );
-
-//         if resampler.delay().is_some() {
-//             process_resampler(&mut resampler, &mut samples, &mut resampled_frame);
-//         }
-//     }
-
-//     decoder.send_eof().unwrap();
-
-//     process_decoder(
-//         decoder,
-//         &mut decoded_frame,
-//         &mut resampler,
-//         &mut resampled_frame,
-//         &mut samples,
-//     );
-
-//     process_resampler(&mut resampler, &mut samples, &mut resampled_frame);
-
-//     samples
-// }
-
 pub struct AudioRenderer {
     data: Vec<AudioSegment>,
     cursor: AudioRendererCursor,
diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs
index 4b53f576b4..d5291b63ad 100644
--- a/crates/editor/src/editor.rs
+++ b/crates/editor/src/editor.rs
@@ -1,10 +1,9 @@
 use std::sync::Arc;
 
-use cap_media::frame_ws::WSFrame;
 use cap_project::{CursorEvents, RecordingMeta, StudioRecordingMeta};
 use cap_rendering::{
     DecodedSegmentFrames, FrameRenderer, ProjectRecordingsMeta, ProjectUniforms,
-    RenderVideoConstants, RendererLayers,
+    RenderVideoConstants, RenderedFrame, RendererLayers,
 };
 use tokio::{
     sync::{mpsc, oneshot},
@@ -26,7 +25,7 @@ pub enum RendererMessage {
 
 pub struct Renderer {
     rx: mpsc::Receiver<RendererMessage>,
-    frame_tx: flume::Sender<WSFrame>,
+    frame_cb: Box<dyn FnMut(RenderedFrame) + Send>,
     render_constants: Arc<RenderVideoConstants>,
     #[allow(unused)]
     total_frames: u32,
@@ -39,7 +38,7 @@ pub struct RendererHandle {
 impl Renderer {
     pub fn spawn(
         render_constants: Arc<RenderVideoConstants>,
-        frame_tx: flume::Sender<WSFrame>,
+        frame_cb: Box<dyn FnMut(RenderedFrame) + Send>,
         recording_meta: &RecordingMeta,
         meta: &StudioRecordingMeta,
     ) -> Result<RendererHandle, String> {
@@ -63,7 +62,7 @@ impl Renderer {
 
         let this = Self {
             rx,
-            frame_tx,
+            frame_cb,
             render_constants,
             total_frames,
         };
@@ -98,24 +97,14 @@ impl Renderer {
                             }
                         }
 
-                        let frame_tx = self.frame_tx.clone();
-
-                        let output_size = uniforms.output_size;
-
                         let frame = frame_renderer
                             .render(segment_frames, uniforms, &cursor, &mut layers)
                             .await
                             .unwrap();
 
-                        frame_tx
-                            .try_send(WSFrame {
-                                data: frame.data,
-                                width: output_size.0,
-                                height: output_size.1,
-                                stride: frame.padded_bytes_per_row,
-                            })
-                            .ok();
-                        finished.send(()).ok();
+                        (self.frame_cb)(frame);
+
+                        let _ = finished.send(());
                     }
                     RendererMessage::Stop { finished } => {
                         if let Some(task) = frame_task.take() {
diff --git a/crates/editor/src/editor_instance.rs b/crates/editor/src/editor_instance.rs
index a80582d46f..c39a1f10a9 100644
--- a/crates/editor/src/editor_instance.rs
+++ b/crates/editor/src/editor_instance.rs
@@ -1,23 +1,20 @@
 use crate::editor;
 use crate::playback::{self, PlaybackHandle};
 use cap_audio::AudioData;
-// use cap_media::feeds::AudioData;
-use cap_media::frame_ws::create_frame_ws;
 use cap_project::StudioRecordingMeta;
 use cap_project::{CursorEvents, ProjectConfiguration, RecordingMeta, RecordingMetaInner, XY};
 use cap_rendering::{
     ProjectRecordingsMeta, ProjectUniforms, RecordingSegmentDecoders, RenderVideoConstants,
-    SegmentVideoPaths, get_duration,
+    RenderedFrame, SegmentVideoPaths, get_duration,
 };
 use std::ops::Deref;
 use std::{path::PathBuf, sync::Arc};
 use tokio::sync::{Mutex, watch};
-use tokio_util::sync::CancellationToken;
 use tracing::trace;
 
 pub struct EditorInstance {
     pub project_path: PathBuf,
-    pub ws_port: u16,
+    // pub ws_port: u16,
     pub recordings: Arc<ProjectRecordingsMeta>,
     pub renderer: Arc<editor::RendererHandle>,
     pub render_constants: Arc<RenderVideoConstants>,
@@ -28,7 +25,7 @@ pub struct EditorInstance {
         watch::Sender<ProjectConfiguration>,
         watch::Receiver<ProjectConfiguration>,
     ),
-    ws_shutdown_token: CancellationToken,
+    // ws_shutdown_token: CancellationToken,
     pub segments: Arc<Vec<Segment>>,
     meta: RecordingMeta,
 }
@@ -37,6 +34,7 @@ impl EditorInstance {
     pub async fn new(
         project_path: PathBuf,
         on_state_change: impl Fn(&EditorState) + Send + Sync + 'static,
+        frame_cb: Box<dyn FnMut(RenderedFrame) + Send>,
     ) -> Result<Arc<Self>, String> {
         sentry::configure_scope(|scope| {
             scope.set_tag("crate", "editor");
@@ -59,10 +57,6 @@ impl EditorInstance {
 
         let segments = create_segments(&recording_meta, meta).await?;
 
-        let (frame_tx, frame_rx) = flume::bounded(4);
-
-        let (ws_port, ws_shutdown_token) = create_frame_ws(frame_rx).await;
-
         let render_constants = Arc::new(
             RenderVideoConstants::new(&recordings.segments, recording_meta.clone(), meta.clone())
                 .await
@@ -71,7 +65,7 @@ impl EditorInstance {
 
         let renderer = Arc::new(editor::Renderer::spawn(
             render_constants.clone(),
-            frame_tx,
+            frame_cb,
             &recording_meta,
             meta,
         )?);
@@ -81,7 +75,6 @@ impl EditorInstance {
         let this = Arc::new(Self {
             project_path,
             recordings,
-            ws_port,
             renderer,
             render_constants,
             state: Arc::new(Mutex::new(EditorState {
@@ -92,7 +85,6 @@ impl EditorInstance {
             on_state_change: Box::new(on_state_change),
             preview_tx,
             project_config: watch::channel(project),
-            ws_shutdown_token,
             segments: Arc::new(segments),
             meta: recording_meta,
         });
@@ -125,10 +117,6 @@ impl EditorInstance {
             task.await.ok(); // Await the task to ensure it's fully stopped
         }
 
-        // Stop WebSocket server
-        trace!("Shutting down WebSocket server");
-        self.ws_shutdown_token.cancel();
-
         // Stop renderer
         trace!("Stopping renderer");
         self.renderer.stop().await;
diff --git a/crates/editor/src/lib.rs b/crates/editor/src/lib.rs
index 2b9befe8a8..e20cc8fb3a 100644
--- a/crates/editor/src/lib.rs
+++ b/crates/editor/src/lib.rs
@@ -1,7 +1,9 @@
+mod audio;
 mod editor;
 mod editor_instance;
 mod playback;
 mod segments;
 
+pub use audio::AudioRenderer;
 pub use editor_instance::{EditorInstance, EditorState, Segment, create_segments};
 pub use segments::get_audio_segments;
diff --git a/crates/editor/src/playback.rs b/crates/editor/src/playback.rs
index 1eb5265525..4a762ca28c 100644
--- a/crates/editor/src/playback.rs
+++ b/crates/editor/src/playback.rs
@@ -1,8 +1,5 @@
-use std::{sync::Arc, time::Duration};
-
+use cap_audio::FromSampleBytes;
 use cap_media::MediaError;
-use cap_media::data::FromSampleBytes;
-use cap_media::feeds::{AudioPlaybackBuffer, AudioSegment};
 use cap_media_info::AudioInfo;
 use cap_project::{ProjectConfiguration, XY};
 use cap_rendering::{ProjectUniforms, RenderVideoConstants};
@@ -10,12 +7,16 @@ use cpal::{
     BufferSize, SampleFormat,
     traits::{DeviceTrait, HostTrait, StreamTrait},
 };
+use std::{sync::Arc, time::Duration};
 use tokio::{sync::watch, time::Instant};
 use tracing::{error, info};
 
-use crate::editor;
-use crate::editor_instance::Segment;
-use crate::segments::get_audio_segments;
+use crate::{
+    audio::{AudioPlaybackBuffer, AudioSegment},
+    editor,
+    editor_instance::Segment,
+    segments::get_audio_segments,
+};
 
 pub struct Playback {
     pub renderer: Arc<editor::RendererHandle>,
diff --git a/crates/editor/src/segments.rs b/crates/editor/src/segments.rs
index d8bf221a1a..a9422a3d98 100644
--- a/crates/editor/src/segments.rs
+++ b/crates/editor/src/segments.rs
@@ -1,6 +1,7 @@
-use cap_media::feeds::{AudioSegment, AudioSegmentTrack};
-
-use crate::Segment;
+use crate::{
+    Segment,
+    audio::{AudioSegment, AudioSegmentTrack},
+};
 
 pub fn get_audio_segments(segments: &[Segment]) -> Vec<AudioSegment> {
     segments
diff --git a/crates/export/src/mp4.rs b/crates/export/src/mp4.rs
index cbd73400ee..f18ce7fac4 100644
--- a/crates/export/src/mp4.rs
+++ b/crates/export/src/mp4.rs
@@ -1,8 +1,7 @@
 use std::{path::PathBuf, time::Duration};
 
 use crate::ExporterBase;
-use cap_editor::get_audio_segments;
-use cap_media::feeds::AudioRenderer;
+use cap_editor::{AudioRenderer, get_audio_segments};
 use cap_media_encoders::{AACEncoder, AudioEncoder, H264Encoder, MP4File, MP4Input};
 use cap_media_info::{RawVideoFormat, VideoInfo};
 use cap_project::XY;
diff --git a/crates/fail/src/lib.rs b/crates/fail/src/lib.rs
index 8c3efc7f9d..de8f2038ad 100644
--- a/crates/fail/src/lib.rs
+++ b/crates/fail/src/lib.rs
@@ -22,15 +22,16 @@ macro_rules! fail {
     ($name:literal) => {
         #[cfg(debug_assertions)]
         {
+            const NAME: &'static str = concat!(env!("CARGO_PKG_NAME"), "::", $name);
+
             $crate::private::inventory::submit! {
-                $crate::Fail { name: $name }
+                $crate::Fail { name: NAME }
             }
 
-            let name: &str = $name;
-            let should_fail = $crate::private::should_fail(name);
+            let should_fail = $crate::private::should_fail(NAME);
 
             if should_fail {
-                panic!("Purposely panicked at '{name}'")
+                panic!("Purposely panicked at '{NAME}'")
             }
         }
     };
@@ -41,15 +42,16 @@ macro_rules! fail_err {
     ($name:literal, $value:expr) => {
         #[cfg(debug_assertions)]
         {
+            const NAME: &'static str = concat!(env!("CARGO_PKG_NAME"), "::", $name);
+
             $crate::private::inventory::submit! {
-                $crate::Fail { name: $name }
+                $crate::Fail { name: NAME }
             }
 
-            let name: &str = $name;
-            let should_fail = $crate::private::should_fail(name);
+            let should_fail = $crate::private::should_fail(NAME);
 
             if should_fail {
-                eprintln!("Purposely Err'd at '{name}'");
+                eprintln!("Purposely Err'd at '{NAME}'");
                 Err($value)?;
             }
         }
diff --git a/crates/ffmpeg-hw-device/Cargo.toml b/crates/ffmpeg-hw-device/Cargo.toml
index cbca7b7a65..1217ac6512 100644
--- a/crates/ffmpeg-hw-device/Cargo.toml
+++ b/crates/ffmpeg-hw-device/Cargo.toml
@@ -5,7 +5,6 @@ edition = "2024"
 
 [dependencies]
 ffmpeg.workspace = true
-ffmpeg-sys-next.workspace = true
 
 [lints]
 workspace = true
diff --git a/crates/ffmpeg-hw-device/src/lib.rs b/crates/ffmpeg-hw-device/src/lib.rs
index c9f88ed2a3..2cc314acba 100644
--- a/crates/ffmpeg-hw-device/src/lib.rs
+++ b/crates/ffmpeg-hw-device/src/lib.rs
@@ -6,11 +6,11 @@ use std::{
 use ffmpeg::{
     codec,
     frame::{self, Video},
-};
-use ffmpeg_sys_next::{
-    AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX, AVBufferRef, AVCodecContext, AVCodecHWConfig,
-    AVHWDeviceType, AVPixelFormat, av_buffer_ref, av_buffer_unref, av_hwdevice_ctx_create,
-    av_hwframe_transfer_data, avcodec_get_hw_config,
+    sys::{
+        AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX, AVBufferRef, AVCodecContext, AVCodecHWConfig,
+        AVHWDeviceType, AVPixelFormat, av_buffer_ref, av_buffer_unref, av_hwdevice_ctx_create,
+        av_hwframe_transfer_data, avcodec_get_hw_config,
+    },
 };
 
 thread_local! {
diff --git a/crates/media-encoders/src/h264.rs b/crates/media-encoders/src/h264.rs
index 5974d6b646..e0a3c1c7a6 100644
--- a/crates/media-encoders/src/h264.rs
+++ b/crates/media-encoders/src/h264.rs
@@ -161,9 +161,10 @@ impl H264Encoder {
                 }
                 Err(e) => {
                     tracing::error!(
-                        "Failed to convert frame: {:?} from format {:?} to YUV420P",
+                        "Failed to convert frame: {} from format {:?} to {:?}",
                         e,
-                        frame.format()
+                        frame.format(),
+                        converter.output().format
                     );
                     // Return early as we can't process this frame
                     return;
diff --git a/crates/media/Cargo.toml b/crates/media/Cargo.toml
index 1f0dcf0236..4e6d7fbdf5 100644
--- a/crates/media/Cargo.toml
+++ b/crates/media/Cargo.toml
@@ -17,17 +17,16 @@ cap-flags = { path = "../flags" }
 cap-audio = { path = "../audio" }
 cap-camera = { path = "../camera", features = ["specta", "serde"] }
 cap-camera-ffmpeg = { path = "../camera-ffmpeg" }
+cap-displays = { path = "../displays" }
 cap-fail = { path = "../fail" }
 cap-media-encoders = { path = "../media-encoders" }
 cap-media-info = { path = "../media-info" }
 cpal.workspace = true
 ffmpeg.workspace = true
-ffmpeg-sys-next.workspace = true
 flume.workspace = true
 indexmap = "2.5.0"
 num-traits = "0.2.19"
 ringbuf = "0.4.7"
-scap.workspace = true
 serde = { workspace = true }
 specta.workspace = true
 tempfile = "3.12.0"
@@ -40,6 +39,9 @@ image = { version = "0.25.2", features = ["gif"] }
 gif = "0.13.1"
 tokio-util = "0.7.15"
 kameo = "0.17.2"
+scap = { workspace = true }
+sync_wrapper = "1.0.2"
+scap-ffmpeg = { path = "../scap-ffmpeg" }
 
 [target.'cfg(target_os = "macos")'.dependencies]
 cidre = { workspace = true }
@@ -50,6 +52,7 @@ objc = "0.2.7"
 objc-foundation = "0.1.1"
 objc2-foundation = { version = "0.2.2", features = ["NSValue"] }
 screencapturekit = "0.3.5"
+scap-screencapturekit = { path = "../scap-screencapturekit" }
 
 [target.'cfg(target_os = "windows")'.dependencies]
 windows = { workspace = true, features = [
@@ -64,6 +67,7 @@ windows = { workspace = true, features = [
 ] }
 windows-capture = { workspace = true }
 cap-camera-windows = { path = "../camera-windows" }
+scap-direct3d = { path = "../scap-direct3d" }
 
 [dev-dependencies]
 inquire = "0.7.5"
diff --git a/crates/media/src/data.rs b/crates/media/src/data.rs
deleted file mode 100644
index 304babd737..0000000000
--- a/crates/media/src/data.rs
+++ /dev/null
@@ -1,46 +0,0 @@
-pub use ffmpeg::format::{
-    pixel::Pixel,
-    sample::{Sample, Type},
-};
-pub use ffmpeg::util::{
-    channel_layout::ChannelLayout,
-    frame::{Audio as FFAudio, Frame as FFFrame, Video as FFVideo},
-    rational::Rational as FFRational,
-};
-pub use ffmpeg::{Error as FFError, Packet as FFPacket, error::EAGAIN};
-
-/// # Safety
-/// The input slice must be aligned to the size of `f32`.
-pub unsafe fn cast_f32_slice_to_bytes(slice: &[f32]) -> &[u8] {
-    unsafe { std::slice::from_raw_parts(slice.as_ptr() as *const u8, slice.len() * f32::BYTE_SIZE) }
-}
-
-/// # Safety
-/// The input slice must be aligned to the size of `f32`.
-pub unsafe fn cast_bytes_to_f32_slice(slice: &[u8]) -> &[f32] {
-    unsafe {
-        std::slice::from_raw_parts(slice.as_ptr() as *const f32, slice.len() / f32::BYTE_SIZE)
-    }
-}
-
-pub trait FromSampleBytes: cpal::SizedSample + std::fmt::Debug + Send + 'static {
-    const BYTE_SIZE: usize;
-
-    fn from_bytes(bytes: &[u8]) -> Self;
-}
-
-macro_rules! sample_bytes {
-    ( $( $num:ty, $size:literal ),* ) => (
-        $(
-            impl FromSampleBytes for $num {
-                const BYTE_SIZE: usize = $size;
-
-                fn from_bytes(bytes: &[u8]) -> Self {
-                    Self::from_le_bytes(bytes.try_into().expect("Incorrect byte slice length"))
-                }
-            }
-        )*
-    )
-}
-
-sample_bytes!(u8, 1, i16, 2, i32, 4, i64, 8, f32, 4, f64, 8);
diff --git a/crates/media/src/data/video.rs b/crates/media/src/data/video.rs
deleted file mode 100644
index d58a4a5ebe..0000000000
--- a/crates/media/src/data/video.rs
+++ /dev/null
@@ -1,35 +0,0 @@
-pub struct FFVideo(ffmpeg::frame::Video);
-
-impl FFVideo {
-    pub fn new(format: ffmpeg::format::Pixel, width: u32, height: u32) -> Self {
-        Self(ffmpeg::frame::Video::new(format, width, height))
-    }
-
-    pub fn data(&self, index: usize) -> &[u8] {
-        self.0.data(index)
-    }
-
-    pub fn data_mut(&mut self, index: usize) -> &mut [u8] {
-        self.0.data_mut(index)
-    }
-
-    pub fn stride(&self, index: usize) -> usize {
-        self.0.stride(index)
-    }
-
-    pub fn as_mut_ptr(&mut self) -> *mut ffmpeg_sys_next::AVFrame {
-        self.0.as_mut_ptr()
-    }
-
-    pub fn as_ptr(&self) -> *const ffmpeg_sys_next::AVFrame {
-        self.0.as_ptr()
-    }
-
-    pub fn convert_with(
-        &mut self,
-        context: &mut ffmpeg::software::scaling::Context,
-        output: &mut FFVideo,
-    ) -> Result<(), ffmpeg::Error> {
-        unsafe { context.run(&self.0, &mut output.0) }
-    }
-}
diff --git a/crates/media/src/lib.rs b/crates/media/src/lib.rs
index e367ef741f..f72127d80a 100644
--- a/crates/media/src/lib.rs
+++ b/crates/media/src/lib.rs
@@ -9,13 +9,6 @@ use std::borrow::Cow;
 use cap_media_info::AudioInfoError;
 use thiserror::Error;
 
-pub mod data;
-pub mod feeds;
-pub mod frame_ws;
-pub mod pipeline;
-pub mod platform;
-pub mod sources;
-
 pub fn init() -> Result<(), MediaError> {
     ffmpeg::init()?;
 
diff --git a/crates/media/src/pipeline/clock/mod.rs b/crates/media/src/pipeline/clock/mod.rs
deleted file mode 100644
index 27e5583bd0..0000000000
--- a/crates/media/src/pipeline/clock/mod.rs
+++ /dev/null
@@ -1,31 +0,0 @@
-mod real_time;
-mod recorded;
-
-pub use real_time::*;
-pub use recorded::*;
-
-pub trait PipelineClock: Clone + Send + 'static {
-    fn start(&mut self);
-
-    fn stop(&mut self);
-
-    fn running(&self) -> bool;
-}
-
-// TODO: Move to utils mod?
-pub trait CloneFrom<T> {
-    fn clone_from(value: &T) -> Self;
-}
-
-pub trait CloneInto<T> {
-    fn clone_into(&self) -> T;
-}
-
-impl<S, T> CloneFrom<T> for S
-where
-    T: CloneInto<S>,
-{
-    fn clone_from(value: &T) -> Self {
-        value.clone_into()
-    }
-}
diff --git a/crates/media/src/pipeline/clock/real_time.rs b/crates/media/src/pipeline/clock/real_time.rs
deleted file mode 100644
index ac7cd6eb8d..0000000000
--- a/crates/media/src/pipeline/clock/real_time.rs
+++ /dev/null
@@ -1,160 +0,0 @@
-use std::sync::{
-    Arc, RwLock,
-    atomic::{AtomicBool, AtomicU64, Ordering},
-};
-use std::time::{Duration, Instant};
-
-use super::{CloneInto, PipelineClock};
-
-pub trait LocalTimestamp: Sized + Clone {
-    fn elapsed_since(&self, other: &Self) -> Duration;
-}
-
-impl LocalTimestamp for () {
-    fn elapsed_since(&self, _other: &Self) -> Duration {
-        Duration::ZERO
-    }
-}
-
-impl LocalTimestamp for Instant {
-    fn elapsed_since(&self, other: &Self) -> Duration {
-        self.duration_since(*other)
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct RawNanoseconds(pub u64);
-
-impl LocalTimestamp for RawNanoseconds {
-    fn elapsed_since(&self, other: &Self) -> Duration {
-        Duration::from_nanos(self.0) - Duration::from_nanos(other.0)
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct RealTimeClock<T: LocalTimestamp> {
-    local_start_time: Option<Instant>,
-    global_start_time: Arc<RwLock<Instant>>,
-    first_local_timestamp: Option<T>,
-    // We could store the `Duration` here, but that would be more expensive than using an atomic integer.
-    resume_offset_nanoseconds: Arc<AtomicU64>,
-    running: Arc<AtomicBool>,
-}
-
-impl<Source: LocalTimestamp, Target: LocalTimestamp> CloneInto<RealTimeClock<Target>>
-    for RealTimeClock<Source>
-{
-    fn clone_into(&self) -> RealTimeClock<Target> {
-        let RealTimeClock {
-            global_start_time,
-            resume_offset_nanoseconds,
-            running,
-            ..
-        } = self.clone();
-
-        RealTimeClock {
-            global_start_time,
-            resume_offset_nanoseconds,
-            running,
-            local_start_time: None,
-            first_local_timestamp: None,
-        }
-    }
-}
-
-impl<T: LocalTimestamp> Default for RealTimeClock<T> {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
-impl<T: LocalTimestamp> RealTimeClock<T> {
-    pub fn init() -> RealTimeClock<()> {
-        RealTimeClock::<()>::new()
-    }
-
-    pub fn new() -> Self {
-        Self {
-            local_start_time: None,
-            global_start_time: Arc::new(RwLock::new(Instant::now())),
-            first_local_timestamp: None,
-            resume_offset_nanoseconds: Arc::new(AtomicU64::new(0)),
-            running: Arc::new(AtomicBool::new(false)),
-        }
-    }
-
-    fn set_running(&self, value: bool) {
-        self.running.store(value, Ordering::Release);
-    }
-
-    fn resume_offset(&self) -> Duration {
-        let nanos = self.resume_offset_nanoseconds.load(Ordering::Acquire);
-
-        Duration::from_nanos(nanos)
-    }
-
-    fn update_resume_offset(&self, delta: Duration) {
-        let delta_nanos = delta.as_nanos().try_into().unwrap();
-        self.resume_offset_nanoseconds
-            .fetch_add(delta_nanos, Ordering::AcqRel);
-    }
-
-    pub fn timestamp_for(&mut self, local: T) -> Option<i64> {
-        let now = Instant::now();
-
-        if !self.running.load(Ordering::Acquire) {
-            return None;
-        }
-
-        if let Ok(global_start_time) = self.global_start_time.read() {
-            // TODO: Cache some of this calculation thread-locally?
-
-            if self.local_start_time.is_none()
-                || self.local_start_time.unwrap() < *global_start_time
-            {
-                println!("Just resumed, resetting local state");
-                self.local_start_time = Some(now);
-                self.first_local_timestamp = Some(local.clone());
-            }
-
-            let local_start_time = self.local_start_time.as_ref().unwrap();
-            let first_local_timestamp = self.first_local_timestamp.as_ref().unwrap();
-
-            let total_offset =
-                local_start_time.duration_since(*global_start_time) + self.resume_offset();
-            let elapsed_time = local.elapsed_since(first_local_timestamp) + total_offset;
-            let timestamp = elapsed_time.as_micros().try_into().unwrap();
-
-            Some(timestamp)
-        } else {
-            // TODO: Try clearing poison? Right now just re-panic if the panic that caused the poison somehow did not stop the app
-            panic!("Pipeline clock has been poisoned!")
-        }
-    }
-}
-
-impl PipelineClock for RealTimeClock<()> {
-    fn running(&self) -> bool {
-        self.running.load(Ordering::Acquire)
-    }
-
-    fn start(&mut self) {
-        if !self.running() {
-            let mut start_time = self.global_start_time.write().unwrap();
-
-            let now = Instant::now();
-            *start_time = now;
-            self.set_running(true);
-        }
-    }
-
-    fn stop(&mut self) {
-        if self.running() {
-            self.set_running(false);
-
-            let now = Instant::now();
-            let start_time = self.global_start_time.read().unwrap();
-            self.update_resume_offset(now - *start_time);
-        }
-    }
-}
diff --git a/crates/media/src/pipeline/clock/recorded.rs b/crates/media/src/pipeline/clock/recorded.rs
deleted file mode 100644
index 027a6cd763..0000000000
--- a/crates/media/src/pipeline/clock/recorded.rs
+++ /dev/null
@@ -1,56 +0,0 @@
-use std::sync::{
-    Arc,
-    atomic::{AtomicBool, AtomicU32, Ordering},
-};
-
-use super::{CloneInto, PipelineClock};
-
-#[derive(Debug, Clone)]
-pub struct RecordedClock {
-    running: Arc<AtomicBool>,
-    start_frame_offset: Arc<AtomicU32>,
-    total_frame_duration: f64,
-}
-
-impl RecordedClock {
-    pub fn new(duration: f64, fps: u32) -> Self {
-        Self {
-            running: Arc::new(AtomicBool::new(false)),
-            start_frame_offset: Arc::new(AtomicU32::new(0)),
-            total_frame_duration: duration * f64::from(fps),
-        }
-    }
-
-    pub fn seek(&mut self, playhead: u32) {
-        self.start_frame_offset.store(playhead, Ordering::Release);
-    }
-
-    pub fn playhead_ratio(&self) -> f64 {
-        let playhead = self.start_frame_offset.load(Ordering::Acquire);
-        f64::from(playhead) / self.total_frame_duration
-    }
-
-    fn set_running(&self, value: bool) {
-        self.running.store(value, Ordering::Release);
-    }
-}
-
-impl CloneInto<RecordedClock> for RecordedClock {
-    fn clone_into(&self) -> RecordedClock {
-        self.clone()
-    }
-}
-
-impl PipelineClock for RecordedClock {
-    fn running(&self) -> bool {
-        self.running.load(Ordering::Acquire)
-    }
-
-    fn start(&mut self) {
-        self.set_running(true);
-    }
-
-    fn stop(&mut self) {
-        self.set_running(false);
-    }
-}
diff --git a/crates/media/src/platform/macos.rs b/crates/media/src/platform/macos.rs
deleted file mode 100644
index 796cf2eb6e..0000000000
--- a/crates/media/src/platform/macos.rs
+++ /dev/null
@@ -1,488 +0,0 @@
-use cidre::{cg, cv};
-use cocoa::{base::id, foundation::NSDictionary};
-use core_foundation::{
-    array::CFArrayGetCount,
-    base::FromVoid,
-    dictionary::CFDictionaryGetValue,
-    number::{CFNumberGetValue, CFNumberRef, kCFNumberIntType},
-    string::{CFString, CFStringRef},
-};
-use core_graphics::{
-    base::boolean_t,
-    display::{CFArrayGetValueAtIndex, CFDictionaryRef, CGDisplay, CGDisplayBounds, CGRect},
-    window::{
-        CGWindowListCopyWindowInfo, kCGNullWindowID, kCGWindowBounds, kCGWindowLayer,
-        kCGWindowListExcludeDesktopElements, kCGWindowListOptionOnScreenOnly, kCGWindowName,
-        kCGWindowNumber, kCGWindowOwnerName, kCGWindowOwnerPID,
-    },
-};
-use std::{collections::HashMap, ffi::c_void};
-
-use crate::platform::{Bounds, LogicalPosition, LogicalSize, Window};
-
-use super::LogicalBounds;
-
-#[link(name = "CoreGraphics", kind = "framework")]
-unsafe extern "C" {
-    fn CGRectMakeWithDictionaryRepresentation(
-        dict: CFDictionaryRef,
-        rect: *mut CGRect,
-    ) -> boolean_t;
-}
-
-pub fn get_on_screen_windows() -> Vec<Window> {
-    let mut windows = Vec::new();
-
-    unsafe {
-        let cf_win_array = CGWindowListCopyWindowInfo(
-            kCGWindowListExcludeDesktopElements | kCGWindowListOptionOnScreenOnly,
-            kCGNullWindowID,
-        );
-
-        let window_count = match cf_win_array.is_null() {
-            true => 0,
-            false => CFArrayGetCount(cf_win_array),
-        };
-
-        for i in 0..window_count {
-            let window_cf_dictionary_ref =
-                CFArrayGetValueAtIndex(cf_win_array, i) as CFDictionaryRef;
-
-            if window_cf_dictionary_ref.is_null() {
-                continue;
-            }
-
-            let level = match get_number_value_from_dict(window_cf_dictionary_ref, kCGWindowLayer) {
-                Some(value) => value,
-                None => continue,
-            };
-
-            let window_id =
-                match get_number_value_from_dict(window_cf_dictionary_ref, kCGWindowNumber) {
-                    Some(value) => value,
-                    None => continue,
-                };
-
-            let name = match get_string_value_from_dict(window_cf_dictionary_ref, kCGWindowName) {
-                Some(value) => value,
-                None => continue,
-            };
-
-            let owner_name =
-                match get_string_value_from_dict(window_cf_dictionary_ref, kCGWindowOwnerName) {
-                    Some(value) => value,
-                    None => continue,
-                };
-
-            if owner_name != "Window Server" && level == 0 && !name.is_empty() {
-                let process_id =
-                    match get_number_value_from_dict(window_cf_dictionary_ref, kCGWindowOwnerPID) {
-                        Some(value) => value,
-                        None => continue,
-                    };
-                if let Some(bounds) = get_window_bounds(window_cf_dictionary_ref) {
-                    windows.push(Window {
-                        window_id,
-                        name,
-                        owner_name,
-                        process_id,
-                        bounds,
-                    });
-                }
-            }
-        }
-    }
-
-    windows
-}
-
-unsafe fn get_nullable_value_from_dict(
-    cf_dictionary_ref: CFDictionaryRef,
-    key: CFStringRef,
-) -> Option<*const c_void> {
-    let value_ref = unsafe { CFDictionaryGetValue(cf_dictionary_ref, key as *const c_void) };
-    if value_ref.is_null() {
-        return None;
-    }
-
-    Some(value_ref)
-}
-
-unsafe fn get_number_value_from_dict(
-    cf_dictionary_ref: CFDictionaryRef,
-    key: CFStringRef,
-) -> Option<u32> {
-    unsafe { get_nullable_value_from_dict(cf_dictionary_ref, key) }.and_then(|value_ref| {
-        let mut value: u32 = 0;
-        let value_ptr = &mut value as *mut _ as *mut c_void;
-        match unsafe { CFNumberGetValue(value_ref as CFNumberRef, kCFNumberIntType, value_ptr) } {
-            true => Some(value),
-            false => None,
-        }
-    })
-}
-
-unsafe fn get_string_value_from_dict(
-    cf_dictionary_ref: CFDictionaryRef,
-    key: CFStringRef,
-) -> Option<String> {
-    unsafe { get_nullable_value_from_dict(cf_dictionary_ref, key) }
-        .map(|value_ref| unsafe { CFString::from_void(value_ref).to_string() })
-}
-
-fn get_window_bounds(window_cf_dictionary_ref: CFDictionaryRef) -> Option<Bounds> {
-    unsafe { get_nullable_value_from_dict(window_cf_dictionary_ref, kCGWindowBounds) }.map(
-        |value_ref| {
-            let rect: CGRect = {
-                let mut rect = unsafe { std::mem::zeroed() };
-                unsafe { CGRectMakeWithDictionaryRepresentation(value_ref.cast(), &mut rect) };
-                rect
-            };
-
-            Bounds {
-                x: rect.origin.x,
-                y: rect.origin.y,
-                width: rect.size.width,
-                height: rect.size.height,
-            }
-        },
-    )
-}
-
-pub fn bring_window_to_focus(window_id: u32) {
-    // TODO(PJ): Replace with Accessibility API once I remember how it works
-    use std::io::Write;
-    use std::process::Command;
-    use tempfile::NamedTempFile;
-
-    println!("Attempting to bring window {window_id} to focus");
-
-    // Get the window information associated with the window id
-    let windows = get_on_screen_windows();
-    if let Some(window) = windows.into_iter().find(|w| w.window_id == window_id) {
-        let process_id = window.process_id;
-        let window_title = window.name.clone();
-        let bounds_x = window.bounds.x;
-        let bounds_y = window.bounds.y;
-        let bounds_width = window.bounds.width;
-        let bounds_height = window.bounds.height;
-        let should_focus = true;
-
-        // Prepare the AppleScript
-        let apple_script = r#"
-        on run argv
-            set processId to item 1 of argv as number
-            set windowTitle to item 2 of argv
-            set boundsX to item 3 of argv as number
-            set boundsY to item 4 of argv as number
-            set boundsWidth to item 5 of argv as number
-            set boundsHeight to item 6 of argv as number
-            set shouldFocus to item 7 of argv as boolean or true
-
-            log "processId: " & processId
-            log "windowTitle: " & windowTitle
-            log "boundsX: " & boundsX
-            log "boundsY: " & boundsY
-            log "boundsWidth: " & boundsWidth
-            log "boundsHeight: " & boundsHeight
-
-            tell application "System Events"
-                set appProcess to first process whose unix id is processId
-                set frontmost of appProcess to true
-
-                tell appProcess
-                    set appWindowsCount to count of windows
-                    log "appWindowsCount: " & appWindowsCount
-
-                    if appWindowsCount is equal to 1 then
-                        perform action "AXRaise" of first window
-                        log "--found window--"
-                        return
-                    end if
-
-                    repeat with checkedWindow in windows
-                        tell checkedWindow
-                            if title contains windowTitle and position is equal to {boundsX, boundsY} and size is equal to {boundsWidth, boundsHeight} then
-                                perform action "AXRaise" of checkedWindow
-                                log "--found window--"
-                                exit repeat
-                            end if
-                        end tell
-                    end repeat
-                end tell
-            end tell
-        end run
-        "#;
-
-        // Prepare arguments
-        let args = vec![
-            process_id.to_string(),
-            window_title.clone(),
-            bounds_x.to_string(),
-            bounds_y.to_string(),
-            bounds_width.to_string(),
-            bounds_height.to_string(),
-            should_focus.to_string(),
-        ];
-
-        // Write the AppleScript to a temporary file
-        let mut script_file = NamedTempFile::new().expect("Failed to create temp file");
-        script_file
-            .write_all(apple_script.as_bytes())
-            .expect("Failed to write to temp file");
-
-        // Execute the AppleScript with arguments
-        let output = Command::new("osascript")
-            .arg(script_file.path())
-            .args(&args)
-            .output();
-
-        match output {
-            Ok(output) => {
-                if output.status.success() {
-                    println!("Successfully executed AppleScript");
-                } else {
-                    let error_message = String::from_utf8_lossy(&output.stderr);
-                    eprintln!("AppleScript execution failed: {error_message}");
-                }
-            }
-            Err(e) => eprintln!("Failed to execute AppleScript: {e}"),
-        }
-
-        println!("Finished attempt to bring window {window_id} to focus");
-    } else {
-        eprintln!("Window with id {window_id} not found");
-    }
-}
-
-pub fn display_names() -> HashMap<u32, String> {
-    use cocoa::appkit::NSScreen;
-    use cocoa::base::nil;
-    use cocoa::foundation::{NSArray, NSString};
-    use objc::{msg_send, *};
-    use std::ffi::CStr;
-
-    unsafe {
-        let screens = NSScreen::screens(nil);
-        let screen_count = NSArray::count(screens);
-
-        let mut names = HashMap::new();
-
-        for i in 0..screen_count {
-            let screen: *mut objc::runtime::Object = screens.objectAtIndex(i);
-
-            let name: id = msg_send![screen, localizedName];
-            let name = CStr::from_ptr(NSString::UTF8String(name))
-                .to_string_lossy()
-                .to_string();
-
-            let device_description = NSScreen::deviceDescription(screen);
-            let num = NSDictionary::valueForKey_(
-                device_description,
-                NSString::alloc(nil).init_str("NSScreenNumber"),
-            ) as id;
-            let num: *const objc2_foundation::NSNumber = num.cast();
-            let num = { &*num };
-            let num = num.as_u32();
-
-            names.insert(num, name);
-        }
-
-        names
-    }
-}
-
-pub fn monitor_bounds(id: u32) -> Bounds {
-    use cocoa::appkit::NSScreen;
-    use cocoa::base::nil;
-    use cocoa::foundation::{NSArray, NSDictionary, NSString};
-
-    unsafe {
-        let screens = NSScreen::screens(nil);
-        let screen_count = NSArray::count(screens);
-
-        for i in 0..screen_count {
-            let screen: *mut objc::runtime::Object = screens.objectAtIndex(i);
-
-            let device_description = NSScreen::deviceDescription(screen);
-            let num = NSDictionary::valueForKey_(
-                device_description,
-                NSString::alloc(nil).init_str("NSScreenNumber"),
-            ) as id;
-            let num: *const objc2_foundation::NSNumber = num.cast();
-            let num = { &*num };
-            let num = num.as_u32();
-
-            if num == id {
-                let frame = NSScreen::frame(screen);
-
-                return Bounds {
-                    x: frame.origin.x,
-                    y: frame.origin.y,
-                    width: frame.size.width,
-                    height: frame.size.height,
-                };
-            }
-        }
-
-        Bounds {
-            x: 0.0,
-            y: 0.0,
-            width: 0.0,
-            height: 0.0,
-        }
-    }
-}
-
-pub fn get_display_refresh_rate(
-    display_id: core_graphics::display::CGDirectDisplayID,
-) -> Result<u32, String> {
-    use core_graphics::display::CGDisplay;
-
-    let display = CGDisplay::new(display_id);
-    let rate = display
-        .display_mode()
-        .ok_or("no display_mode")?
-        .refresh_rate()
-        .round() as u32;
-
-    if rate == 0 {
-        // adapted from https://github.com/mpv-player/mpv/commit/eacf22e42a6bbce8a32e64f5563ac431122c1186
-        let link = cv::DisplayLink::with_cg_display(cg::DirectDisplayId(display.id))
-            .map_err(|e| format!("with_cg_display / {e}"))?;
-
-        let t = link.nominal_output_video_refresh_period();
-
-        if !t.flags.contains(cv::TimeFlags::IS_INDEFINITE) {
-            Ok((t.scale as f64 / t.value as f64).round() as u32)
-        } else {
-            Err("refresh rate is indefinite".to_string())
-        }
-    } else {
-        Ok(rate)
-    }
-}
-
-pub fn display_for_window(
-    window: core_graphics::window::CGWindowID,
-) -> Option<core_graphics::display::CGDisplay> {
-    use core_foundation::array::CFArray;
-    use core_graphics::{
-        display::{CFDictionary, CGDisplay, CGRect},
-        window::{create_description_from_array, kCGWindowBounds},
-    };
-
-    let descriptions = create_description_from_array(CFArray::from_copyable(&[window]))?;
-
-    let window_bounds = CGRect::from_dict_representation(
-        &descriptions
-            .get(0)?
-            .get(unsafe { kCGWindowBounds })
-            .downcast::<CFDictionary>()?,
-    )?;
-
-    for id in CGDisplay::active_displays().ok()? {
-        let display = CGDisplay::new(id);
-        if window_bounds.is_intersects(&display.bounds()) {
-            return Some(display);
-        }
-    }
-
-    None
-}
-
-pub fn primary_monitor_bounds() -> Bounds {
-    let display = CGDisplay::main();
-    let height = display.pixels_high();
-    let width = display.pixels_wide();
-    let bounds = unsafe { CGDisplayBounds(display.id) };
-
-    Bounds {
-        x: bounds.origin.x,
-        y: bounds.origin.y,
-        width: width as f64,
-        height: height as f64,
-    }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct MonitorHandle(pub u32);
-
-impl MonitorHandle {
-    pub fn primary() -> Self {
-        let display = CGDisplay::main();
-        Self(display.id)
-    }
-
-    pub fn list_all() -> Vec<Self> {
-        use cocoa::appkit::NSScreen;
-        use cocoa::base::nil;
-        use cocoa::foundation::{NSArray, NSDictionary, NSString};
-
-        let mut ret = vec![];
-
-        unsafe {
-            let screens = NSScreen::screens(nil);
-            let screen_count = NSArray::count(screens);
-
-            for i in 0..screen_count {
-                let screen: *mut objc::runtime::Object = screens.objectAtIndex(i);
-
-                let device_description = NSScreen::deviceDescription(screen);
-                let num = NSDictionary::valueForKey_(
-                    device_description,
-                    NSString::alloc(nil).init_str("NSScreenNumber"),
-                ) as id;
-                let num: *const objc2_foundation::NSNumber = num.cast();
-                let num = { &*num };
-                let num = num.as_u32();
-
-                ret.push(Self(num));
-            }
-
-            ret
-        }
-    }
-}
-
-pub fn logical_monitor_bounds(monitor_id: u32) -> Option<LogicalBounds> {
-    use cocoa::appkit::NSScreen;
-    use cocoa::base::nil;
-    use cocoa::foundation::{NSArray, NSDictionary, NSString};
-
-    unsafe {
-        let screens = NSScreen::screens(nil);
-        let screen_count = NSArray::count(screens);
-
-        for i in 0..screen_count {
-            let screen: *mut objc::runtime::Object = screens.objectAtIndex(i);
-
-            let device_description = NSScreen::deviceDescription(screen);
-            let num = NSDictionary::valueForKey_(
-                device_description,
-                NSString::alloc(nil).init_str("NSScreenNumber"),
-            ) as id;
-            let num: *const objc2_foundation::NSNumber = num.cast();
-            let num = { &*num };
-            let num = num.as_u32();
-
-            if num == monitor_id {
-                let frame = NSScreen::frame(screen);
-
-                return Some(LogicalBounds {
-                    position: LogicalPosition {
-                        x: frame.origin.x,
-                        y: frame.origin.y
-                            + (CGDisplay::main().pixels_high() as f64 - frame.size.height),
-                    },
-                    size: LogicalSize {
-                        width: frame.size.width,
-                        height: frame.size.height,
-                    },
-                });
-            }
-        }
-
-        None
-    }
-}
diff --git a/crates/media/src/platform/mod.rs b/crates/media/src/platform/mod.rs
deleted file mode 100644
index 894d20d4b5..0000000000
--- a/crates/media/src/platform/mod.rs
+++ /dev/null
@@ -1,47 +0,0 @@
-use serde::{Deserialize, Serialize};
-use specta::Type;
-
-#[cfg(target_os = "macos")]
-#[path = "macos.rs"]
-mod platform_impl;
-
-#[cfg(target_os = "windows")]
-#[path = "win.rs"]
-mod platform_impl;
-
-pub use platform_impl::*;
-
-#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Type)]
-pub struct Bounds {
-    pub x: f64,
-    pub y: f64,
-    pub width: f64,
-    pub height: f64,
-}
-
-#[derive(Debug)]
-pub struct Window {
-    pub window_id: u32,
-    pub name: String,
-    pub owner_name: String,
-    pub process_id: u32,
-    pub bounds: Bounds,
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct LogicalPosition {
-    pub x: f64,
-    pub y: f64,
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct LogicalSize {
-    pub width: f64,
-    pub height: f64,
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct LogicalBounds {
-    pub position: LogicalPosition,
-    pub size: LogicalSize,
-}
diff --git a/crates/media/src/platform/win.rs b/crates/media/src/platform/win.rs
deleted file mode 100644
index 2ae664fde3..0000000000
--- a/crates/media/src/platform/win.rs
+++ /dev/null
@@ -1,299 +0,0 @@
-use std::collections::HashMap;
-use std::ffi::{OsString, c_void};
-use std::os::windows::ffi::OsStringExt;
-use std::path::PathBuf;
-
-use super::{Bounds, LogicalBounds, LogicalPosition, LogicalSize, Window};
-
-use tracing::debug;
-use windows::Win32::{
-    Foundation::{CloseHandle, FALSE, HWND, LPARAM, RECT, TRUE},
-    Graphics::{
-        Dwm::{DWMWA_CLOAKED, DWMWA_EXTENDED_FRAME_BOUNDS, DwmGetWindowAttribute},
-        Gdi::{
-            DEVMODEW, DISPLAY_DEVICEW, EnumDisplayDevicesW, EnumDisplayMonitors,
-            EnumDisplaySettingsW, GetMonitorInfoW, HDC, HMONITOR, MONITOR_DEFAULTTONULL,
-            MONITORINFO, MONITORINFOEXW, MonitorFromWindow,
-        },
-    },
-    System::Threading::{
-        OpenProcess, PROCESS_NAME_FORMAT, PROCESS_QUERY_LIMITED_INFORMATION,
-        QueryFullProcessImageNameW,
-    },
-    UI::HiDpi::GetDpiForWindow,
-    UI::WindowsAndMessaging::{
-        EnumWindows, GetWindowTextLengthW, GetWindowTextW, GetWindowThreadProcessId,
-        IsWindowVisible, SetForegroundWindow,
-    },
-};
-use windows::core::{BOOL, PCWSTR, PWSTR};
-
-#[inline]
-pub fn bring_window_to_focus(window_id: u32) {
-    let _ = unsafe { SetForegroundWindow(HWND(window_id as *mut c_void)) };
-}
-
-unsafe fn pid_to_exe_path(pid: u32) -> Result<PathBuf, windows::core::Error> {
-    let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) }?;
-    if handle.is_invalid() {
-        tracing::error!("Invalid PID {}", pid);
-    }
-    let mut lpexename = [0u16; 1024];
-    let mut lpdwsize = lpexename.len() as u32;
-
-    let query = unsafe {
-        QueryFullProcessImageNameW(
-            handle,
-            PROCESS_NAME_FORMAT::default(),
-            windows::core::PWSTR(lpexename.as_mut_ptr()),
-            &mut lpdwsize,
-        )
-    };
-    unsafe { CloseHandle(handle) }.ok();
-    query?;
-
-    let os_str = &OsString::from_wide(&lpexename[..lpdwsize as usize]);
-    Ok(PathBuf::from(os_str))
-}
-
-pub fn get_on_screen_windows() -> Vec<Window> {
-    let mut windows = Vec::<Window>::new();
-
-    unsafe extern "system" fn enum_window_proc(hwnd: HWND, lparam: LPARAM) -> BOOL {
-        if hwnd.is_invalid() {
-            return TRUE;
-        }
-        let windows = unsafe { &mut *(lparam.0 as *mut Vec<Window>) };
-
-        if !unsafe { IsWindowVisible(hwnd) }.as_bool() {
-            return TRUE;
-        }
-
-        let mut pvattribute_cloaked = 0u32;
-        unsafe {
-            DwmGetWindowAttribute(
-                hwnd,
-                DWMWA_CLOAKED,
-                &mut pvattribute_cloaked as *mut _ as *mut std::ffi::c_void,
-                std::mem::size_of::<u32>() as u32,
-            )
-        }
-        .ok();
-
-        if pvattribute_cloaked != 0 {
-            return TRUE;
-        }
-
-        let mut process_id = 0;
-        let _thrad_id = unsafe { GetWindowThreadProcessId(hwnd, Some(&mut process_id)) };
-
-        let wnamelen = unsafe { GetWindowTextLengthW(hwnd) };
-        if wnamelen == 0 {
-            return TRUE;
-        }
-        let mut wname = [0u16; 512];
-        let len = unsafe { GetWindowTextW(hwnd, &mut wname) };
-
-        let owner_process_path = match unsafe { pid_to_exe_path(process_id) } {
-            Ok(path) => path,
-            Err(_) => return TRUE,
-        };
-
-        if owner_process_path.starts_with("C:\\Windows\\SystemApps") {
-            return TRUE;
-        }
-
-        let owner_name = match owner_process_path.file_stem() {
-            Some(exe_name) => exe_name.to_string_lossy().into_owned(),
-            None => owner_process_path.to_string_lossy().into_owned(),
-        };
-
-        // Windows 10 build 1607 or later
-        // Credits: TAO src/platform_impl/windows/dpi.rs
-        const BASE_DPI: u32 = 96;
-        let _dpi = match unsafe { GetDpiForWindow(hwnd) } {
-            0 => BASE_DPI,
-            dpi => dpi,
-        } as i32;
-
-        let mut rect = RECT::default();
-        unsafe {
-            DwmGetWindowAttribute(
-                hwnd,
-                DWMWA_EXTENDED_FRAME_BOUNDS,
-                &mut rect as *mut _ as *mut std::ffi::c_void,
-                std::mem::size_of::<RECT>() as u32,
-            )
-        }
-        .ok();
-
-        let rect_left = rect.left as f64;
-        let rect_top = rect.top as f64;
-        let rect_right = rect.right as f64;
-        let rect_bottom = rect.bottom as f64;
-
-        let window = Window {
-            window_id: hwnd.0 as u32,
-            name: String::from_utf16_lossy(&wname[..len as usize]),
-            owner_name,
-            process_id,
-            bounds: Bounds {
-                x: rect_left.max(0.0),
-                y: rect_top.max(0.0),
-                width: rect_right - rect_left,
-                height: rect_bottom - rect_top,
-            },
-        };
-
-        windows.push(window);
-        TRUE
-    }
-
-    let _ = unsafe {
-        EnumWindows(
-            Some(enum_window_proc),
-            LPARAM(core::ptr::addr_of_mut!(windows) as isize),
-        )
-    };
-    windows
-}
-
-pub fn monitor_bounds(id: u32) -> Bounds {
-    let bounds = None::<Bounds>;
-
-    unsafe extern "system" fn monitor_enum_proc(
-        hmonitor: HMONITOR,
-        _hdc: HDC,
-        _lprc_clip: *mut RECT,
-        lparam: LPARAM,
-    ) -> BOOL {
-        let (target_id, bounds) = unsafe { &mut *(lparam.0 as *mut (u32, Option<Bounds>)) };
-
-        let mut minfo = MONITORINFOEXW::default();
-        minfo.monitorInfo.cbSize = std::mem::size_of::<MONITORINFOEXW>() as u32;
-        if !unsafe { GetMonitorInfoW(hmonitor, &mut minfo as *mut MONITORINFOEXW as *mut _) }
-            .as_bool()
-        {
-            return TRUE;
-        }
-
-        let mut display_device = DISPLAY_DEVICEW::default();
-        #[allow(clippy::field_reassign_with_default)]
-        {
-            display_device.cb = std::mem::size_of::<DISPLAY_DEVICEW>() as u32;
-        }
-
-        if !unsafe {
-            EnumDisplayDevicesW(
-                PWSTR(minfo.szDevice.as_ptr() as _),
-                0,
-                &mut display_device,
-                0,
-            )
-        }
-        .as_bool()
-        {
-            return TRUE;
-        }
-
-        let id = hmonitor.0 as u32;
-
-        if id == *target_id {
-            let rect = minfo.monitorInfo.rcMonitor;
-            *bounds = Some(Bounds {
-                x: rect.left as f64,
-                y: rect.top as f64,
-                width: (rect.right - rect.left) as f64,
-                height: (rect.bottom - rect.top) as f64,
-            });
-            return FALSE;
-        }
-        TRUE
-    }
-
-    let mut lparams = (id, bounds);
-    let _ = unsafe {
-        EnumDisplayMonitors(
-            None,
-            None,
-            Some(monitor_enum_proc),
-            LPARAM(core::ptr::addr_of_mut!(lparams) as isize),
-        )
-    };
-
-    // If we didn't find the monitor with the given ID, log a warning and return a default bounds
-    if lparams.1.is_none() {
-        debug!("Could not find monitor with ID: {}", id);
-        return Bounds {
-            x: 0.0,
-            y: 0.0,
-            width: 1920.0, // Default to a common resolution
-            height: 1080.0,
-        };
-    }
-
-    lparams.1.unwrap()
-}
-
-pub fn logical_monitor_bounds(id: u32) -> Option<LogicalBounds> {
-    let bounds = monitor_bounds(id);
-    Some(LogicalBounds {
-        position: LogicalPosition {
-            x: bounds.x,
-            y: bounds.y,
-        },
-        size: LogicalSize {
-            width: bounds.width,
-            height: bounds.height,
-        },
-    })
-}
-
-pub fn display_names() -> HashMap<u32, String> {
-    let mut names = HashMap::new();
-
-    for window in windows_capture::monitor::Monitor::enumerate().unwrap_or_default() {
-        let Ok(name) = window.device_string() else {
-            continue;
-        };
-
-        names.insert(window.as_raw_hmonitor() as u32, name);
-    }
-
-    names
-}
-
-pub fn get_display_refresh_rate(monitor: HMONITOR) -> Result<u32, String> {
-    let mut monitorinfoexw: MONITORINFOEXW = unsafe { std::mem::zeroed() };
-    monitorinfoexw.monitorInfo.cbSize = std::mem::size_of::<MONITORINFOEXW>() as u32;
-
-    unsafe { GetMonitorInfoW(monitor, &mut monitorinfoexw.monitorInfo as *mut MONITORINFO) }
-        .ok()
-        .map_err(|e| e.to_string())?;
-
-    let mut dev_mode: DEVMODEW = unsafe { std::mem::zeroed() };
-    dev_mode.dmSize = std::mem::size_of::<DEVMODEW>() as u16;
-
-    let device_name = PCWSTR::from_raw(monitorinfoexw.szDevice.as_ptr());
-
-    unsafe {
-        EnumDisplaySettingsW(
-            device_name,
-            windows::Win32::Graphics::Gdi::ENUM_CURRENT_SETTINGS,
-            &mut dev_mode,
-        )
-    }
-    .ok()
-    .map_err(|e| e.to_string())?;
-
-    Ok(dev_mode.dmDisplayFrequency)
-}
-
-pub fn display_for_window(window: HWND) -> Option<HMONITOR> {
-    let hwmonitor = unsafe { MonitorFromWindow(window, MONITOR_DEFAULTTONULL) };
-    if hwmonitor.is_invalid() {
-        None
-    } else {
-        Some(hwmonitor)
-    }
-}
diff --git a/crates/media/src/sources/screen_capture.rs b/crates/media/src/sources/screen_capture.rs
deleted file mode 100644
index b3284906e7..0000000000
--- a/crates/media/src/sources/screen_capture.rs
+++ /dev/null
@@ -1,1020 +0,0 @@
-use cap_media_info::{AudioInfo, PlanarData, RawVideoFormat, VideoInfo};
-use cpal::traits::{DeviceTrait, HostTrait};
-use ffmpeg::{format::Sample, frame};
-use ffmpeg_sys_next::AV_TIME_BASE_Q;
-use flume::Sender;
-use scap::{
-    Target,
-    capturer::{Area, Capturer, Options, Point, Resolution as ScapResolution, Size},
-    frame::{Frame, FrameType, VideoFrame},
-};
-
-use serde::{Deserialize, Serialize};
-use specta::Type;
-use std::{collections::HashMap, ops::ControlFlow, rc::Rc, time::SystemTime};
-use tracing::{debug, error, info, trace, warn};
-#[cfg(target_os = "windows")]
-use windows::Win32::{Foundation::HWND, Graphics::Gdi::HMONITOR};
-
-use crate::{
-    MediaError,
-    pipeline::{clock::*, control::Control, task::PipelineSourceTask},
-    platform::{self, Bounds, Window, logical_monitor_bounds},
-};
-
-static EXCLUDED_WINDOWS: &[&str] = &[
-    "Cap Camera",
-    "Cap Recordings Overlay",
-    "Cap In Progress Recording",
-];
-
-#[derive(Debug, Clone, Serialize, Deserialize, Type)]
-pub struct CaptureWindow {
-    pub id: u32,
-    pub owner_name: String,
-    pub name: String,
-    pub bounds: Bounds,
-    pub refresh_rate: u32,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize, Type)]
-pub struct CaptureScreen {
-    pub id: u32,
-    pub name: String,
-    pub refresh_rate: u32,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize, Type)]
-pub struct CaptureArea {
-    pub screen: CaptureScreen,
-    pub bounds: Bounds,
-}
-
-#[derive(Debug, Clone, Copy, Serialize, Deserialize, Type)]
-#[serde(rename_all = "camelCase", tag = "variant")]
-pub enum ScreenCaptureTarget {
-    Window { id: u32 },
-    Screen { id: u32 },
-    Area { screen: u32, bounds: Bounds },
-}
-
-impl ScreenCaptureTarget {
-    // only available on mac and windows
-    pub fn primary_display() -> Self {
-        ScreenCaptureTarget::Screen {
-            id: scap::get_main_display().id,
-        }
-    }
-
-    pub fn get_target(&self) -> Option<scap::Target> {
-        let targets = scap::get_all_targets();
-
-        match self {
-            ScreenCaptureTarget::Window { id } => targets.into_iter().find(|t| match t {
-                scap::Target::Window(window) => window.id == *id,
-                _ => false,
-            }),
-            ScreenCaptureTarget::Screen { id } => targets.into_iter().find(|t| match t {
-                scap::Target::Display(screen) => screen.id == *id,
-                _ => false,
-            }),
-            ScreenCaptureTarget::Area { screen, .. } => targets.into_iter().find(|t| match t {
-                scap::Target::Display(display) => display.id == *screen,
-                _ => false,
-            }),
-        }
-    }
-
-    pub fn get_title(&self) -> Option<String> {
-        let target = self.get_target();
-
-        match target {
-            None => None,
-            Some(scap::Target::Window(window)) => Some(window.title.clone()),
-            Some(scap::Target::Display(screen)) => {
-                let names = crate::platform::display_names();
-
-                Some(
-                    names
-                        .get(&screen.id)
-                        .cloned()
-                        .unwrap_or_else(|| screen.title.clone()),
-                )
-            }
-        }
-    }
-}
-
-pub struct ScreenCaptureSource<TCaptureFormat: ScreenCaptureFormat> {
-    target: ScreenCaptureTarget,
-    output_type: Option<FrameType>,
-    fps: u32,
-    video_info: VideoInfo,
-    options: Rc<Options>,
-    show_camera: bool,
-    force_show_cursor: bool,
-    bounds: Bounds,
-    // logical display size
-    display_size: (f32, f32),
-    video_tx: Sender<(TCaptureFormat::VideoFormat, f64)>,
-    audio_tx: Option<Sender<(ffmpeg::frame::Audio, f64)>>,
-    _phantom: std::marker::PhantomData<TCaptureFormat>,
-    start_time: SystemTime,
-}
-
-impl<T: ScreenCaptureFormat> std::fmt::Debug for ScreenCaptureSource<T> {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("ScreenCaptureSource")
-            .field("target", &self.target)
-            .field("bounds", &self.bounds)
-            // .field("output_resolution", &self.output_resolution)
-            .field("output_type", &self.output_type)
-            .field("fps", &self.fps)
-            .field("video_info", &self.video_info)
-            .field(
-                "audio_info",
-                &self.audio_tx.as_ref().map(|_| self.audio_info()),
-            )
-            .finish()
-    }
-}
-
-pub trait ScreenCaptureFormat {
-    type VideoFormat;
-
-    fn audio_info() -> AudioInfo;
-}
-
-impl ScreenCaptureFormat for AVFrameCapture {
-    type VideoFormat = ffmpeg::frame::Video;
-
-    fn audio_info() -> AudioInfo {
-        let host = cpal::default_host();
-        let output_device = host.default_output_device().unwrap();
-        let supported_config = output_device.default_output_config().unwrap();
-
-        let mut info = AudioInfo::from_stream_config(&supported_config);
-
-        info.sample_format = Sample::F32(ffmpeg::format::sample::Type::Packed);
-
-        info
-    }
-}
-
-unsafe impl<T: ScreenCaptureFormat> Send for ScreenCaptureSource<T> {}
-unsafe impl<T: ScreenCaptureFormat> Sync for ScreenCaptureSource<T> {}
-
-impl<TCaptureFormat: ScreenCaptureFormat> Clone for ScreenCaptureSource<TCaptureFormat> {
-    fn clone(&self) -> Self {
-        Self {
-            target: self.target,
-            output_type: self.output_type,
-            fps: self.fps,
-            video_info: self.video_info,
-            options: self.options.clone(),
-            show_camera: self.show_camera,
-            force_show_cursor: self.force_show_cursor,
-            bounds: self.bounds,
-            display_size: self.display_size,
-            video_tx: self.video_tx.clone(),
-            audio_tx: self.audio_tx.clone(),
-            _phantom: std::marker::PhantomData,
-            start_time: self.start_time,
-        }
-    }
-}
-
-struct OptionsConfig {
-    scap_target: scap::Target,
-    bounds: Bounds,
-    crop_area: Option<Area>,
-    display_size: (f32, f32),
-}
-
-pub struct CropRatio {
-    pub position: (f32, f32),
-    pub size: (f32, f32),
-}
-
-impl<TCaptureFormat: ScreenCaptureFormat> ScreenCaptureSource<TCaptureFormat> {
-    #[allow(clippy::too_many_arguments)]
-    pub async fn init(
-        target: &ScreenCaptureTarget,
-        output_type: Option<FrameType>,
-        show_camera: bool,
-        force_show_cursor: bool,
-        max_fps: u32,
-        video_tx: Sender<(TCaptureFormat::VideoFormat, f64)>,
-        audio_tx: Option<Sender<(ffmpeg::frame::Audio, f64)>>,
-        start_time: SystemTime,
-    ) -> Result<Self, String> {
-        cap_fail::fail!("media::screen_capture::init");
-
-        let OptionsConfig {
-            scap_target,
-            bounds,
-            crop_area,
-            display_size,
-        } = Self::get_options_config(target)?;
-
-        let fps = get_target_fps(&scap_target).map_err(|e| format!("target_fps / {e}"))?;
-        let fps = fps.min(max_fps);
-
-        let captures_audio = audio_tx.is_some();
-
-        let mut this = Self {
-            target: *target,
-            output_type,
-            fps,
-            video_info: VideoInfo::from_raw(RawVideoFormat::Bgra, 0, 0, 0),
-            options: std::rc::Rc::new(Default::default()),
-            bounds,
-            display_size,
-            show_camera,
-            force_show_cursor,
-            video_tx,
-            audio_tx,
-            _phantom: std::marker::PhantomData,
-            start_time,
-        };
-
-        let options = this.create_options(scap_target, crop_area, captures_audio)?;
-
-        this.options = std::rc::Rc::new(options);
-
-        #[cfg(target_os = "macos")]
-        let video_size = {
-            let [x, y] = scap::capturer::get_output_frame_size(&this.options);
-            (x, y)
-        };
-        #[cfg(windows)]
-        // not sure how reliable this is for the general case so just use it for screen capture for now
-        let video_size = if matches!(target, ScreenCaptureTarget::Screen { .. }) {
-            let [x, y] = scap::capturer::get_output_frame_size(&this.options);
-            (x, y)
-        } else {
-            (bounds.width as u32, bounds.height as u32)
-        };
-
-        this.video_info =
-            VideoInfo::from_raw(RawVideoFormat::Bgra, video_size.0, video_size.1, fps);
-
-        Ok(this)
-    }
-
-    pub fn get_bounds(&self) -> &Bounds {
-        &self.bounds
-    }
-
-    pub fn crop_ratio(&self) -> CropRatio {
-        if let Some(crop_area) = &self.options.crop_area {
-            CropRatio {
-                position: (
-                    crop_area.origin.x as f32 / self.display_size.0,
-                    crop_area.origin.y as f32 / self.display_size.1,
-                ),
-                size: (
-                    crop_area.size.width as f32 / self.display_size.0,
-                    crop_area.size.height as f32 / self.display_size.1,
-                ),
-            }
-        } else {
-            CropRatio {
-                position: (0.0, 0.0),
-                size: (1.0, 1.0),
-            }
-        }
-    }
-
-    fn get_options_config(target: &ScreenCaptureTarget) -> Result<OptionsConfig, String> {
-        let targets = scap::get_all_targets();
-
-        Ok(match target {
-            ScreenCaptureTarget::Window { id } => {
-                let windows = list_windows();
-
-                let (mut window_info, target) = windows
-                    .into_iter()
-                    .find(|t| t.0.id == *id)
-                    .ok_or_else(|| "Capture window not found".to_string())?;
-
-                let Target::Display(display) = display_for_target(&target, &targets)
-                    .ok_or_else(|| "Screen for capture window not found".to_string())?
-                else {
-                    unreachable!()
-                };
-
-                let id = {
-                    #[cfg(target_os = "macos")]
-                    {
-                        display.raw_handle.0
-                    }
-
-                    #[cfg(windows)]
-                    {
-                        display.raw_handle.0 as u32
-                    }
-                };
-
-                let monitor_bounds = logical_monitor_bounds(id).unwrap();
-
-                window_info.bounds.x -= monitor_bounds.position.x;
-                window_info.bounds.y -= monitor_bounds.position.y;
-
-                fn div_by_2able(n: f64) -> f64 {
-                    n + n % 2.0
-                }
-
-                let crop = Area {
-                    size: Size {
-                        width: div_by_2able(window_info.bounds.width),
-                        height: div_by_2able(window_info.bounds.height),
-                    },
-                    origin: Point {
-                        x: div_by_2able(window_info.bounds.x),
-                        y: div_by_2able(window_info.bounds.y),
-                    },
-                };
-
-                OptionsConfig {
-                    scap_target: Target::Display(display),
-                    bounds: Bounds {
-                        x: crop.origin.x,
-                        y: crop.origin.y,
-                        width: crop.size.width,
-                        height: crop.size.height,
-                    },
-                    crop_area: Some(crop),
-                    display_size: (
-                        monitor_bounds.size.width as f32,
-                        monitor_bounds.size.height as f32,
-                    ),
-                }
-            }
-            ScreenCaptureTarget::Screen { id } => {
-                let screens = list_screens();
-
-                let (screen_info, target) = screens
-                    .into_iter()
-                    .find(|(i, _t)| i.id == *id)
-                    .ok_or_else(|| "Target for screen capture not found".to_string())?;
-
-                let bounds = platform::monitor_bounds(screen_info.id);
-
-                OptionsConfig {
-                    scap_target: target,
-                    bounds,
-                    crop_area: None,
-                    display_size: (bounds.width as f32, bounds.height as f32),
-                }
-            }
-            ScreenCaptureTarget::Area { screen, bounds } => {
-                let screen_bounds = platform::monitor_bounds(*screen);
-
-                let screens = list_screens();
-                let screen = screens
-                    .into_iter()
-                    .find_map(|(i, t)| (i.id == *screen).then_some(t))
-                    .ok_or_else(|| "Target for screen capture not found".to_string())?;
-
-                OptionsConfig {
-                    scap_target: screen,
-                    bounds: *bounds,
-                    crop_area: Some(Area {
-                        size: Size {
-                            width: bounds.width,
-                            height: bounds.height,
-                        },
-                        origin: Point {
-                            x: bounds.x,
-                            y: bounds.y,
-                        },
-                    }),
-                    display_size: (screen_bounds.width as f32, screen_bounds.height as f32),
-                }
-            }
-        })
-    }
-
-    fn create_options(
-        &self,
-        target: scap::Target,
-        crop_area: Option<Area>,
-        captures_audio: bool,
-    ) -> Result<Options, String> {
-        let targets = scap::get_all_targets();
-
-        let excluded_targets: Vec<scap::Target> = targets
-            .iter()
-            .filter(|target| match target {
-                Target::Window(scap_window) => {
-                    if scap_window.title == "Cap Camera" && self.show_camera {
-                        false
-                    } else {
-                        EXCLUDED_WINDOWS.contains(&scap_window.title.as_str())
-                    }
-                }
-                Target::Display(_) => false,
-            })
-            .cloned()
-            .collect();
-
-        debug!("configured target: {:#?}", self.target);
-
-        Ok(Options {
-            fps: self.fps,
-            show_cursor: self.force_show_cursor,
-            show_highlight: false,
-            target: Some(target.clone()),
-            crop_area,
-            output_type: self.output_type.unwrap_or(FrameType::BGRAFrame),
-            output_resolution: ScapResolution::Captured,
-            excluded_targets: (!excluded_targets.is_empty()).then_some(excluded_targets),
-            captures_audio,
-            exclude_current_process_audio: true,
-        })
-    }
-
-    pub fn info(&self) -> VideoInfo {
-        self.video_info
-    }
-
-    pub fn audio_info(&self) -> AudioInfo {
-        TCaptureFormat::audio_info()
-    }
-}
-
-#[derive(Debug)]
-pub struct AVFrameCapture;
-
-impl PipelineSourceTask for ScreenCaptureSource<AVFrameCapture> {
-    type Clock = RealTimeClock<RawNanoseconds>;
-
-    // #[instrument(skip_all)]
-    fn run(
-        &mut self,
-        _clock: Self::Clock,
-        ready_signal: crate::pipeline::task::PipelineReadySignal,
-        control_signal: crate::pipeline::control::PipelineControlSignal,
-    ) -> Result<(), String> {
-        let video_info = self.video_info;
-        let video_tx = self.video_tx.clone();
-        let audio_tx = self.audio_tx.clone();
-
-        let start_time = self.start_time;
-
-        let mut video_i = 0;
-        let mut audio_i = 0;
-
-        let mut frames_dropped = 0;
-
-        // Frame drop rate tracking state
-        use std::collections::VecDeque;
-        use std::time::{Duration, Instant};
-
-        let mut frame_events: VecDeque<(Instant, bool)> = VecDeque::new();
-        let window_duration = Duration::from_secs(3);
-        let max_drop_rate_threshold = 0.25;
-        let mut last_cleanup = Instant::now();
-        let mut last_log = Instant::now();
-        let log_interval = Duration::from_secs(5);
-
-        // Helper function to clean up old frame events
-        let cleanup_old_events = |frame_events: &mut VecDeque<(Instant, bool)>, now: Instant| {
-            let cutoff = now - window_duration;
-            while let Some(&(timestamp, _)) = frame_events.front() {
-                if timestamp < cutoff {
-                    frame_events.pop_front();
-                } else {
-                    break;
-                }
-            }
-        };
-
-        // Helper function to calculate current drop rate
-        let calculate_drop_rate =
-            |frame_events: &mut VecDeque<(Instant, bool)>| -> (f64, usize, usize) {
-                let now = Instant::now();
-                cleanup_old_events(frame_events, now);
-
-                if frame_events.is_empty() {
-                    return (0.0, 0, 0);
-                }
-
-                let total_frames = frame_events.len();
-                let dropped_frames = frame_events.iter().filter(|(_, dropped)| *dropped).count();
-                let drop_rate = dropped_frames as f64 / total_frames as f64;
-
-                (drop_rate, dropped_frames, total_frames)
-            };
-
-        inner(
-            self,
-            ready_signal,
-            control_signal,
-            |capturer| match capturer.get_next_frame() {
-                Ok(Frame::Video(VideoFrame::BGRA(frame))) => {
-                    video_i += 1;
-
-                    if frame.height == 0 || frame.width == 0 {
-                        return ControlFlow::Continue(());
-                    }
-
-                    let Ok(elapsed) = frame.display_time.duration_since(start_time) else {
-                        warn!("Skipping video frame {video_i} as elapsed time is invalid");
-                        return ControlFlow::Continue(());
-                    };
-
-                    let mut buffer = frame::Video::new(
-                        video_info.pixel_format,
-                        video_info.width,
-                        video_info.height,
-                    );
-
-                    let bytes_per_pixel = 4;
-                    let width_in_bytes = frame.width as usize * bytes_per_pixel;
-                    let height = frame.height as usize;
-
-                    let src_data = &frame.data;
-
-                    let src_stride = src_data.len() / height;
-                    let dst_stride = buffer.stride(0);
-
-                    if src_data.len() < src_stride * height {
-                        warn!("Frame data size mismatch.");
-                        return ControlFlow::Continue(());
-                    }
-
-                    if src_stride < width_in_bytes {
-                        warn!("Source stride is less than expected width in bytes.");
-                        return ControlFlow::Continue(());
-                    }
-
-                    if buffer.data(0).len() < dst_stride * height {
-                        warn!("Destination data size mismatch.");
-                        return ControlFlow::Continue(());
-                    }
-
-                    {
-                        let dst_data = buffer.data_mut(0);
-
-                        for y in 0..height {
-                            let src_offset = y * src_stride;
-                            let dst_offset = y * dst_stride;
-                            dst_data[dst_offset..dst_offset + width_in_bytes].copy_from_slice(
-                                &src_data[src_offset..src_offset + width_in_bytes],
-                            );
-                        }
-                    }
-
-                    buffer.set_pts(Some(
-                        (elapsed.as_secs_f64() * AV_TIME_BASE_Q.den as f64) as i64,
-                    ));
-
-                    // Record frame attempt and check if it was dropped
-                    let now = Instant::now();
-                    let frame_dropped = match video_tx.try_send((buffer, elapsed.as_secs_f64())) {
-                        Err(flume::TrySendError::Disconnected(_)) => {
-                            return ControlFlow::Break(Err(
-                                "Pipeline is unreachable. Shutting down recording".to_string(),
-                            ));
-                        }
-                        Err(flume::TrySendError::Full(_)) => {
-                            warn!("Screen capture sender is full, dropping frame");
-                            frames_dropped += 1;
-                            true
-                        }
-                        _ => false,
-                    };
-
-                    frame_events.push_back((now, frame_dropped));
-
-                    if now.duration_since(last_cleanup) > Duration::from_millis(100) {
-                        cleanup_old_events(&mut frame_events, now);
-                        last_cleanup = now;
-                    }
-
-                    // Check drop rate and potentially exit
-                    let (drop_rate, dropped_count, total_count) =
-                        calculate_drop_rate(&mut frame_events);
-
-                    if drop_rate > max_drop_rate_threshold && total_count >= 10 {
-                        error!(
-                            "High frame drop rate detected: {:.1}% ({}/{} frames in last {}s). Exiting capture.",
-                            drop_rate * 100.0,
-                            dropped_count,
-                            total_count,
-                            window_duration.as_secs()
-                        );
-                        return ControlFlow::Break(Err("Recording can't keep up with screen capture. Try reducing your display's resolution or refresh rate.".to_string()));
-                    }
-
-                    // Periodic logging of drop rate
-                    if now.duration_since(last_log) > log_interval && total_count > 0 {
-                        info!(
-                            "Frame drop rate: {:.1}% ({}/{} frames, total dropped: {})",
-                            drop_rate * 100.0,
-                            dropped_count,
-                            total_count,
-                            frames_dropped
-                        );
-                        last_log = now;
-                    }
-
-                    ControlFlow::Continue(())
-                }
-                Ok(Frame::Audio(frame)) => {
-                    if let Some(audio_tx) = &audio_tx {
-                        let Ok(elapsed) = frame.time().duration_since(start_time) else {
-                            warn!("Skipping audio frame {audio_i} as elapsed time is invalid");
-                            return ControlFlow::Continue(());
-                        };
-                        let mut frame = scap_audio_to_ffmpeg(frame);
-                        frame.set_pts(Some(
-                            (elapsed.as_secs_f64() * AV_TIME_BASE_Q.den as f64) as i64,
-                        ));
-                        let _ = audio_tx.send((frame, elapsed.as_secs_f64()));
-                        audio_i += 1;
-                    }
-                    ControlFlow::Continue(())
-                }
-                Ok(_) => panic!("Unsupported video format"),
-                Err(error) => ControlFlow::Break(Err(format!("Capture error: {error}"))),
-            },
-        )
-    }
-}
-
-fn inner<T: ScreenCaptureFormat>(
-    source: &mut ScreenCaptureSource<T>,
-    ready_signal: crate::pipeline::task::PipelineReadySignal,
-    mut control_signal: crate::pipeline::control::PipelineControlSignal,
-    mut get_frame: impl FnMut(&mut Capturer) -> ControlFlow<Result<(), String>>,
-) -> Result<(), String> {
-    trace!("Preparing screen capture source thread...");
-
-    let maybe_capture_window_id = match &source.target {
-        ScreenCaptureTarget::Window { id } => Some(*id),
-        _ => None,
-    };
-
-    let mut capturer = match Capturer::build(source.options.as_ref().clone()) {
-        Ok(capturer) => capturer,
-        Err(e) => {
-            error!("Failed to build capturer: {e}");
-            let _ = ready_signal.send(Err(MediaError::Any("Failed to build capturer".into())));
-            return Err(e.to_string());
-        }
-    };
-
-    info!("Capturer built");
-
-    let mut capturing = false;
-    let _ = ready_signal.send(Ok(()));
-
-    cap_fail::fail!("macos screen_capture start panic");
-
-    loop {
-        match control_signal.last() {
-            Some(Control::Shutdown) | None => {
-                trace!("Received shutdown signal");
-                if capturing {
-                    capturer.stop_capture();
-                    info!("Capturer stopped")
-                }
-                break;
-            }
-            Some(Control::Play) => {
-                if !capturing {
-                    if let Some(window_id) = maybe_capture_window_id {
-                        crate::platform::bring_window_to_focus(window_id);
-                    }
-                    capturer.start_capture();
-                    capturing = true;
-
-                    info!("Screen recording started.");
-                }
-
-                match get_frame(&mut capturer) {
-                    ControlFlow::Break(res) => {
-                        warn!("breaking from loop");
-
-                        if let Err(e) = &res {
-                            error!("Capture loop broke with error: {}", e)
-                        }
-
-                        return res;
-                    }
-                    ControlFlow::Continue(_) => {
-                        continue;
-                    }
-                }
-            }
-        }
-    }
-
-    info!("Shut down screen capture source thread.");
-    Ok(())
-}
-
-#[derive(Debug)]
-pub struct CMSampleBufferCapture;
-
-#[cfg(target_os = "macos")]
-impl ScreenCaptureFormat for CMSampleBufferCapture {
-    type VideoFormat = cidre::arc::R<cidre::cm::SampleBuf>;
-
-    fn audio_info() -> AudioInfo {
-        AudioInfo::new(
-            ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar),
-            48_000,
-            2,
-        )
-        .unwrap()
-    }
-}
-
-#[cfg(target_os = "macos")]
-impl PipelineSourceTask for ScreenCaptureSource<CMSampleBufferCapture> {
-    type Clock = RealTimeClock<RawNanoseconds>;
-
-    fn run(
-        &mut self,
-        _clock: Self::Clock,
-        ready_signal: crate::pipeline::task::PipelineReadySignal,
-        control_signal: crate::pipeline::control::PipelineControlSignal,
-    ) -> Result<(), String> {
-        let video_tx = self.video_tx.clone();
-        let audio_tx = self.audio_tx.clone();
-
-        let start = std::time::SystemTime::now();
-        let start_time_unix = start
-            .duration_since(std::time::UNIX_EPOCH)
-            .expect("Time went backwards")
-            .as_secs_f64();
-        let start_cmtime = cidre::cm::Clock::host_time_clock().time();
-        let start_cmtime = start_cmtime.value as f64 / start_cmtime.scale as f64;
-
-        let start_time_f64 = self
-            .start_time
-            .duration_since(SystemTime::UNIX_EPOCH)
-            .unwrap()
-            .as_secs_f64();
-
-        inner(
-            self,
-            ready_signal,
-            control_signal,
-            |capturer| match capturer.raw().get_next_sample_buffer() {
-                Ok((sample_buffer, typ)) => {
-                    use cidre::sc;
-
-                    #[allow(clippy::useless_transmute)]
-                    let sample_buffer = unsafe {
-                        std::mem::transmute::<_, cidre::arc::R<cidre::cm::SampleBuf>>(sample_buffer)
-                    };
-
-                    let frame_time =
-                        sample_buffer.pts().value as f64 / sample_buffer.pts().scale as f64;
-                    let unix_timestamp = start_time_unix + frame_time - start_cmtime;
-                    let relative_time = unix_timestamp - start_time_f64;
-
-                    match typ {
-                        sc::stream::OutputType::Screen => {
-                            let Some(pixel_buffer) = sample_buffer.image_buf() else {
-                                return ControlFlow::Continue(());
-                            };
-
-                            if pixel_buffer.height() == 0 || pixel_buffer.width() == 0 {
-                                return ControlFlow::Continue(());
-                            }
-
-                            let check_skip_send = || {
-                                cap_fail::fail_err!(
-                                    "media::sources::screen_capture::skip_send",
-                                    ()
-                                );
-
-                                Ok::<(), ()>(())
-                            };
-
-                            if check_skip_send().is_ok()
-                                && video_tx.send((sample_buffer, relative_time)).is_err()
-                            {
-                                error!("Pipeline is unreachable. Shutting down recording.");
-                                return ControlFlow::Continue(());
-                            }
-                        }
-                        sc::stream::OutputType::Audio => {
-                            use ffmpeg::ChannelLayout;
-
-                            let res = || {
-                                cap_fail::fail_err!("screen_capture audio skip", ());
-                                Ok::<(), ()>(())
-                            };
-                            if res().is_err() {
-                                return ControlFlow::Continue(());
-                            }
-
-                            let Some(audio_tx) = &audio_tx else {
-                                return ControlFlow::Continue(());
-                            };
-
-                            let buf_list = sample_buffer.audio_buf_list::<2>().unwrap();
-                            let slice = buf_list.block().as_slice().unwrap();
-
-                            let mut frame = ffmpeg::frame::Audio::new(
-                                ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar),
-                                sample_buffer.num_samples() as usize,
-                                ChannelLayout::STEREO,
-                            );
-                            frame.set_rate(48_000);
-                            let data_bytes_size = buf_list.list().buffers[0].data_bytes_size;
-                            for i in 0..frame.planes() {
-                                use cap_media_info::PlanarData;
-
-                                frame.plane_data_mut(i).copy_from_slice(
-                                    &slice[i * data_bytes_size as usize
-                                        ..(i + 1) * data_bytes_size as usize],
-                                );
-                            }
-
-                            frame.set_pts(Some((relative_time * AV_TIME_BASE_Q.den as f64) as i64));
-
-                            let _ = audio_tx.send((frame, relative_time));
-                        }
-                        _ => {}
-                    }
-
-                    ControlFlow::Continue(())
-                }
-                Err(error) => ControlFlow::Break(Err(format!("Capture error: {error}"))),
-            },
-        )
-    }
-}
-
-pub fn list_screens() -> Vec<(CaptureScreen, Target)> {
-    if !scap::has_permission() {
-        return vec![];
-    }
-
-    let mut targets = vec![];
-    let screens = scap::get_all_targets()
-        .into_iter()
-        .filter_map(|t| match t {
-            Target::Display(screen) => Some(screen),
-            _ => None,
-        })
-        .collect::<Vec<_>>();
-
-    let names = crate::platform::display_names();
-
-    for (idx, screen) in screens.into_iter().enumerate() {
-        targets.push((
-            CaptureScreen {
-                id: screen.id,
-                name: names
-                    .get(&screen.id)
-                    .cloned()
-                    .unwrap_or_else(|| format!("Screen {}", idx + 1)),
-                refresh_rate: {
-                    let Ok(fps) = get_target_fps(&Target::Display(screen.clone())) else {
-                        continue;
-                    };
-
-                    fps
-                },
-            },
-            Target::Display(screen),
-        ));
-    }
-    targets
-}
-
-pub fn list_windows() -> Vec<(CaptureWindow, Target)> {
-    if !scap::has_permission() {
-        return vec![];
-    }
-
-    let targets = scap::get_all_targets();
-
-    let platform_windows: HashMap<u32, Window> = crate::platform::get_on_screen_windows()
-        .into_iter()
-        .map(|window| (window.window_id, window))
-        .collect();
-
-    targets
-        .into_iter()
-        .filter_map(|target| match &target {
-            Target::Window(window) => {
-                let id = window.id;
-                platform_windows.get(&id).map(|platform_window| {
-                    (
-                        CaptureWindow {
-                            id,
-                            owner_name: platform_window.owner_name.clone(),
-                            name: platform_window.name.clone(),
-                            bounds: platform_window.bounds,
-                            refresh_rate: get_target_fps(&target).unwrap_or_default(),
-                        },
-                        target,
-                    )
-                })
-            }
-            Target::Display(_) => None,
-        })
-        .collect()
-}
-
-pub fn get_target_fps(target: &scap::Target) -> Result<u32, String> {
-    #[cfg(target_os = "macos")]
-    match target {
-        scap::Target::Display(display) => platform::get_display_refresh_rate(display.raw_handle.0),
-        scap::Target::Window(window) => platform::get_display_refresh_rate(
-            platform::display_for_window(window.raw_handle)
-                .ok_or_else(|| "failed to get display for window".to_string())?
-                .id,
-        ),
-    }
-    #[cfg(target_os = "windows")]
-    match target {
-        scap::Target::Display(display) => {
-            platform::get_display_refresh_rate(HMONITOR(display.raw_handle.0))
-        }
-        scap::Target::Window(window) => platform::get_display_refresh_rate(
-            platform::display_for_window(HWND(window.raw_handle.0))
-                .ok_or_else(|| "failed to get display for window".to_string())?,
-        ),
-    }
-    #[cfg(not(any(target_os = "windows", target_os = "macos")))]
-    None
-}
-
-fn display_for_target<'a>(
-    target: &'a scap::Target,
-    targets: &'a [scap::Target],
-) -> Option<scap::Target> {
-    match target {
-        scap::Target::Display(_) => Some(target),
-        scap::Target::Window(window) => {
-            #[cfg(target_os = "macos")]
-            {
-                let id = platform::display_for_window(window.raw_handle)?;
-                targets.iter().find(|t| match t {
-                    scap::Target::Display(d) => d.raw_handle.0 == id.id,
-                    _ => false,
-                })
-            }
-            #[cfg(windows)]
-            {
-                let id = platform::display_for_window(HWND(window.raw_handle.0))?;
-                targets.iter().find(|t| match t {
-                    scap::Target::Display(d) => d.raw_handle.0 == id.0,
-                    _ => false,
-                })
-            }
-        }
-    }
-    .cloned()
-}
-
-fn scap_audio_to_ffmpeg(scap_frame: scap::frame::AudioFrame) -> ffmpeg::frame::Audio {
-    use ffmpeg::format::Sample;
-    use scap::frame::AudioFormat;
-
-    let format_typ = if scap_frame.is_planar() {
-        ffmpeg::format::sample::Type::Planar
-    } else {
-        ffmpeg::format::sample::Type::Packed
-    };
-
-    let mut ffmpeg_frame = ffmpeg::frame::Audio::new(
-        match scap_frame.format() {
-            AudioFormat::F32 => Sample::F32(format_typ),
-            AudioFormat::F64 => Sample::F64(format_typ),
-            AudioFormat::I16 => Sample::I16(format_typ),
-            AudioFormat::I32 => Sample::I32(format_typ),
-            AudioFormat::U8 => Sample::U8(format_typ),
-            _ => panic!("Unsupported sample format"),
-        },
-        scap_frame.sample_count(),
-        ffmpeg::ChannelLayout::default(scap_frame.channels() as i32),
-    );
-
-    if scap_frame.is_planar() {
-        for i in 0..scap_frame.planes() {
-            ffmpeg_frame
-                .plane_data_mut(i as usize)
-                .copy_from_slice(scap_frame.plane_data(i as usize));
-        }
-    } else {
-        ffmpeg_frame
-            .data_mut(0)
-            .copy_from_slice(scap_frame.raw_data());
-    }
-
-    ffmpeg_frame.set_rate(scap_frame.rate());
-
-    ffmpeg_frame
-}
diff --git a/crates/recording/Cargo.toml b/crates/recording/Cargo.toml
index 9ab1170c60..25b5cf7e1f 100644
--- a/crates/recording/Cargo.toml
+++ b/crates/recording/Cargo.toml
@@ -7,6 +7,8 @@ edition = "2024"
 workspace = true
 
 [dependencies]
+cap-audio = { path = "../audio" }
+cap-fail = { path = "../fail" }
 cap-project = { path = "../project" }
 cap-media = { path = "../media" }
 cap-flags = { path = "../flags" }
@@ -16,13 +18,14 @@ cap-cursor-capture = { path = "../cursor-capture" }
 cap-media-encoders = { path = "../media-encoders" }
 cap-media-info = { path = "../media-info" }
 cap-cursor-info = { path = "../cursor-info" }
+cap-camera = { path = "../camera" }
+cap-camera-ffmpeg = { path = "../camera-ffmpeg" }
 
 specta.workspace = true
 tokio.workspace = true
 flume.workspace = true
 thiserror.workspace = true
 ffmpeg = { workspace = true }
-scap.workspace = true
 
 serde = { workspace = true }
 serde_json = "1"
@@ -31,12 +34,17 @@ tracing.workspace = true
 device_query = "4.0.1"
 image = "0.25.2"
 either = "1.13.0"
-tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
+tracing-subscriber = { version = "0.3.19" }
 relative-path = "1.9.3"
 futures = { workspace = true }
 tokio-util = "0.7.15"
 sha2 = "0.10.9"
 hex = "0.4.3"
+ringbuf = "0.4.8"
+cpal.workspace = true
+indexmap = "2.10.0"
+kameo = "0.17.2"
+inquire = "0.7.5"
 
 [target.'cfg(target_os = "macos")'.dependencies]
 screencapturekit = "0.3.5"
@@ -44,6 +52,7 @@ cocoa = "0.26.0"
 objc = "0.2.7"
 cidre = { workspace = true }
 objc2-app-kit = "0.3.1"
+scap-screencapturekit = { path = "../scap-screencapturekit" }
 
 [target.'cfg(target_os = "windows")'.dependencies]
 windows = { workspace = true, features = [
@@ -51,6 +60,9 @@ windows = { workspace = true, features = [
 	"Win32_Graphics_Gdi",
 	"Win32_UI_WindowsAndMessaging",
 ] }
+scap-direct3d = { path = "../scap-direct3d" }
+scap-ffmpeg = { path = "../scap-ffmpeg" }
+scap-cpal = { path = "../scap-cpal" }
 
 [dev-dependencies]
 tempfile = "3.20.0"
diff --git a/crates/media/examples/camera.rs b/crates/recording/examples/camera.rs
similarity index 96%
rename from crates/media/examples/camera.rs
rename to crates/recording/examples/camera.rs
index 7426d98520..94b0b1ade7 100644
--- a/crates/media/examples/camera.rs
+++ b/crates/recording/examples/camera.rs
@@ -1,6 +1,6 @@
 use std::fmt::Display;
 
-use cap_media::feeds::{CameraFeed, DeviceOrModelID};
+use cap_recording::feeds::{CameraFeed, DeviceOrModelID};
 use ffmpeg::format::Pixel;
 use image::{ColorType, codecs::jpeg};
 
diff --git a/crates/recording/examples/recording-cli.rs b/crates/recording/examples/recording-cli.rs
index 5e0dbae0bd..7cd7dff9e2 100644
--- a/crates/recording/examples/recording-cli.rs
+++ b/crates/recording/examples/recording-cli.rs
@@ -1,10 +1,17 @@
 use std::time::Duration;
 
-use cap_media::sources::ScreenCaptureTarget;
-use cap_recording::RecordingBaseInputs;
+use cap_displays::Display;
+use cap_recording::{RecordingBaseInputs, screen_capture::ScreenCaptureTarget};
 
 #[tokio::main]
 pub async fn main() {
+    #[cfg(windows)]
+    {
+        use windows::Win32::UI::HiDpi::{PROCESS_PER_MONITOR_DPI_AWARE, SetProcessDpiAwareness};
+
+        unsafe { SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE).unwrap() };
+    }
+
     tracing_subscriber::fmt::init();
 
     let _ = std::fs::remove_dir_all("/tmp/bruh");
@@ -18,12 +25,21 @@ pub async fn main() {
         "test".to_string(),
         dir.path().into(),
         RecordingBaseInputs {
-            capture_target: ScreenCaptureTarget::primary_display(),
-            capture_system_audio: false,
+            capture_target: ScreenCaptureTarget::Display {
+                id: Display::primary().id(),
+            },
+            // ScreenCaptureTarget::Window {
+            //     id: Window::list()
+            //         .into_iter()
+            //         .find(|w| w.owner_name().unwrap_or_default().contains("Brave"))
+            //         .unwrap()
+            //         .id(),
+            // },
+            capture_system_audio: true,
             mic_feed: &None,
         },
         None,
-        false,
+        true,
     )
     .await
     .unwrap();
diff --git a/crates/recording/examples/screen_capture.rs b/crates/recording/examples/screen_capture.rs
new file mode 100644
index 0000000000..409fbb8875
--- /dev/null
+++ b/crates/recording/examples/screen_capture.rs
@@ -0,0 +1,50 @@
+use cap_displays::Window;
+use cap_recording::{
+    pipeline::{control::PipelineControlSignal, task::PipelineSourceTask},
+    sources::{CMSampleBufferCapture, ScreenCaptureSource, ScreenCaptureTarget},
+};
+use std::time::SystemTime;
+
+#[tokio::main]
+async fn main() {
+    tracing_subscriber::fmt::init();
+
+    let (video_tx, video_rx) = flume::unbounded();
+    let (ready_tx, _ready_rx) = flume::unbounded();
+    let (_ctrl_tx, ctrl_rx) = flume::unbounded();
+
+    let mut source = ScreenCaptureSource::<CMSampleBufferCapture>::init(
+        &ScreenCaptureTarget::Window {
+            id: Window::list()
+                .into_iter()
+                .find(|w| w.owner_name().unwrap().contains("Zed"))
+                .unwrap()
+                .id(),
+        },
+        false,
+        60,
+        video_tx,
+        None,
+        SystemTime::now(),
+        tokio::runtime::Handle::current(),
+    )
+    .await
+    .unwrap();
+
+    std::thread::spawn(move || {
+        let _ = source.run(
+            ready_tx,
+            PipelineControlSignal {
+                last_value: None,
+                receiver: ctrl_rx,
+            },
+        );
+    });
+
+    while let Ok((video, _)) = video_rx.recv_async().await {
+        video.image_buf().unwrap();
+        dbg!(video.total_sample_size());
+    }
+
+    tokio::time::sleep(std::time::Duration::from_secs(5)).await;
+}
diff --git a/crates/recording/src/capture_pipeline.rs b/crates/recording/src/capture_pipeline.rs
index fc7a31f646..ec09554fd2 100644
--- a/crates/recording/src/capture_pipeline.rs
+++ b/crates/recording/src/capture_pipeline.rs
@@ -1,3 +1,7 @@
+use cap_media::MediaError;
+
+use cap_media_info::AudioInfo;
+use flume::{Receiver, Sender};
 use std::{
     future::Future,
     path::PathBuf,
@@ -5,37 +9,30 @@ use std::{
     time::SystemTime,
 };
 
-use cap_media::{
-    MediaError,
+use crate::{
+    RecordingError,
     feeds::AudioInputFeed,
-    pipeline::{RealTimeClock, builder::PipelineBuilder},
+    pipeline::builder::PipelineBuilder,
     sources::{
-        AVFrameCapture, AudioInputSource, AudioMixer, ScreenCaptureFormat, ScreenCaptureSource,
-        ScreenCaptureTarget,
+        AudioInputSource, AudioMixer, ScreenCaptureFormat, ScreenCaptureSource,
+        ScreenCaptureTarget, screen_capture,
     },
 };
-use cap_media_encoders::{AACEncoder, AudioEncoder, H264Encoder, MP4File};
-use cap_media_info::AudioInfo;
-use flume::{Receiver, Sender};
-
-use crate::RecordingError;
-
-pub type CapturePipelineBuilder = PipelineBuilder<RealTimeClock<()>>;
 
 pub trait MakeCapturePipeline: ScreenCaptureFormat + std::fmt::Debug + 'static {
     fn make_studio_mode_pipeline(
-        builder: CapturePipelineBuilder,
+        builder: PipelineBuilder,
         source: (
             ScreenCaptureSource<Self>,
             flume::Receiver<(Self::VideoFormat, f64)>,
         ),
         output_path: PathBuf,
-    ) -> Result<(CapturePipelineBuilder, flume::Receiver<f64>), MediaError>
+    ) -> Result<(PipelineBuilder, flume::Receiver<f64>), MediaError>
     where
         Self: Sized;
 
     fn make_instant_mode_pipeline(
-        builder: CapturePipelineBuilder,
+        builder: PipelineBuilder,
         source: (
             ScreenCaptureSource<Self>,
             flume::Receiver<(Self::VideoFormat, f64)>,
@@ -44,22 +41,24 @@ pub trait MakeCapturePipeline: ScreenCaptureFormat + std::fmt::Debug + 'static {
         system_audio: Option<(Receiver<(ffmpeg::frame::Audio, f64)>, AudioInfo)>,
         output_path: PathBuf,
         pause_flag: Arc<AtomicBool>,
-    ) -> impl Future<Output = Result<CapturePipelineBuilder, MediaError>> + Send
+    ) -> impl Future<Output = Result<PipelineBuilder, MediaError>> + Send
     where
         Self: Sized;
 }
 
 #[cfg(target_os = "macos")]
-impl MakeCapturePipeline for cap_media::sources::CMSampleBufferCapture {
+impl MakeCapturePipeline for screen_capture::CMSampleBufferCapture {
     fn make_studio_mode_pipeline(
-        mut builder: CapturePipelineBuilder,
+        mut builder: PipelineBuilder,
         source: (
             ScreenCaptureSource<Self>,
             flume::Receiver<(Self::VideoFormat, f64)>,
         ),
         output_path: PathBuf,
-    ) -> Result<(CapturePipelineBuilder, flume::Receiver<f64>), MediaError> {
+    ) -> Result<(PipelineBuilder, flume::Receiver<f64>), MediaError> {
         let screen_config = source.0.info();
+        tracing::info!("screen config: {:?}", screen_config);
+
         let mut screen_encoder = cap_media_encoders::MP4AVAssetWriterEncoder::init(
             "screen",
             screen_config,
@@ -71,6 +70,8 @@ impl MakeCapturePipeline for cap_media::sources::CMSampleBufferCapture {
 
         let (timestamp_tx, timestamp_rx) = flume::bounded(1);
 
+        builder.spawn_source("screen_capture", source.0);
+
         builder.spawn_task("screen_capture_encoder", move |ready| {
             let mut timestamp_tx = Some(timestamp_tx);
             let _ = ready.send(Ok(()));
@@ -102,13 +103,11 @@ impl MakeCapturePipeline for cap_media::sources::CMSampleBufferCapture {
             result
         });
 
-        builder.spawn_source("screen_capture", source.0);
-
         Ok((builder, timestamp_rx))
     }
 
     async fn make_instant_mode_pipeline(
-        mut builder: CapturePipelineBuilder,
+        mut builder: PipelineBuilder,
         source: (
             ScreenCaptureSource<Self>,
             flume::Receiver<(Self::VideoFormat, f64)>,
@@ -117,7 +116,7 @@ impl MakeCapturePipeline for cap_media::sources::CMSampleBufferCapture {
         system_audio: Option<(Receiver<(ffmpeg::frame::Audio, f64)>, AudioInfo)>,
         output_path: PathBuf,
         pause_flag: Arc<AtomicBool>,
-    ) -> Result<CapturePipelineBuilder, MediaError> {
+    ) -> Result<PipelineBuilder, MediaError> {
         let (audio_tx, audio_rx) = flume::bounded(64);
         let mut audio_mixer = AudioMixer::new(audio_tx);
 
@@ -225,23 +224,26 @@ impl MakeCapturePipeline for cap_media::sources::CMSampleBufferCapture {
     }
 }
 
-impl MakeCapturePipeline for AVFrameCapture {
+#[cfg(windows)]
+impl MakeCapturePipeline for screen_capture::AVFrameCapture {
     fn make_studio_mode_pipeline(
-        mut builder: CapturePipelineBuilder,
+        mut builder: PipelineBuilder,
         source: (
             ScreenCaptureSource<Self>,
             flume::Receiver<(Self::VideoFormat, f64)>,
         ),
         output_path: PathBuf,
-    ) -> Result<(CapturePipelineBuilder, flume::Receiver<f64>), MediaError>
+    ) -> Result<(PipelineBuilder, flume::Receiver<f64>), MediaError>
     where
         Self: Sized,
     {
+        use cap_media_encoders::{H264Encoder, MP4File};
+
         let screen_config = source.0.info();
         let mut screen_encoder = MP4File::init(
             "screen",
             output_path,
-            |o| H264Encoder::builder("screen", screen_config).build(o),
+            |o| H264Encoder::builder("screen", dbg!(screen_config)).build(o),
             |_| None,
         )
         .map_err(|e| MediaError::Any(e.to_string().into()))?;
@@ -258,6 +260,7 @@ impl MakeCapturePipeline for AVFrameCapture {
                 if let Some(timestamp_tx) = timestamp_tx.take() {
                     timestamp_tx.send(frame.1).unwrap();
                 }
+                // dbg!(frame.1);
                 screen_encoder.queue_video_frame(frame.0);
             }
             screen_encoder.finish();
@@ -268,7 +271,7 @@ impl MakeCapturePipeline for AVFrameCapture {
     }
 
     async fn make_instant_mode_pipeline(
-        mut builder: CapturePipelineBuilder,
+        mut builder: PipelineBuilder,
         source: (
             ScreenCaptureSource<Self>,
             flume::Receiver<(Self::VideoFormat, f64)>,
@@ -277,10 +280,12 @@ impl MakeCapturePipeline for AVFrameCapture {
         system_audio: Option<(Receiver<(ffmpeg::frame::Audio, f64)>, AudioInfo)>,
         output_path: PathBuf,
         _pause_flag: Arc<AtomicBool>,
-    ) -> Result<CapturePipelineBuilder, MediaError>
+    ) -> Result<PipelineBuilder, MediaError>
     where
         Self: Sized,
     {
+        use cap_media_encoders::{AACEncoder, AudioEncoder, H264Encoder, MP4File};
+
         let (audio_tx, audio_rx) = flume::bounded(64);
         let mut audio_mixer = AudioMixer::new(audio_tx);
 
@@ -360,14 +365,13 @@ type ScreenCaptureReturn<T> = (
 );
 
 #[cfg(target_os = "macos")]
-pub type ScreenCaptureMethod = cap_media::sources::CMSampleBufferCapture;
+pub type ScreenCaptureMethod = screen_capture::CMSampleBufferCapture;
 
-#[cfg(not(target_os = "macos"))]
-pub type ScreenCaptureMethod = AVFrameCapture;
+#[cfg(windows)]
+pub type ScreenCaptureMethod = screen_capture::AVFrameCapture;
 
 pub async fn create_screen_capture(
     capture_target: &ScreenCaptureTarget,
-    show_camera: bool,
     force_show_cursor: bool,
     max_fps: u32,
     audio_tx: Option<Sender<(ffmpeg::frame::Audio, f64)>>,
@@ -377,15 +381,14 @@ pub async fn create_screen_capture(
 
     ScreenCaptureSource::<ScreenCaptureMethod>::init(
         capture_target,
-        None,
-        show_camera,
         force_show_cursor,
         max_fps,
         video_tx,
         audio_tx,
         start_time,
+        tokio::runtime::Handle::current(),
     )
     .await
     .map(|v| (v, video_rx))
-    .map_err(|e| RecordingError::Media(MediaError::TaskLaunch(e)))
+    .map_err(|e| RecordingError::Media(MediaError::TaskLaunch(e.to_string())))
 }
diff --git a/crates/recording/src/cursor.rs b/crates/recording/src/cursor.rs
index dd2cc7f945..4bc817c5ed 100644
--- a/crates/recording/src/cursor.rs
+++ b/crates/recording/src/cursor.rs
@@ -1,8 +1,7 @@
-use std::{collections::HashMap, path::PathBuf, time::SystemTime};
-
+use cap_cursor_capture::CursorCropBounds;
 use cap_cursor_info::CursorShape;
-use cap_media::platform::Bounds;
 use cap_project::{CursorClickEvent, CursorMoveEvent, XY};
+use std::{collections::HashMap, path::PathBuf, time::SystemTime};
 use tokio::sync::oneshot;
 use tokio_util::sync::CancellationToken;
 
@@ -37,23 +36,21 @@ impl CursorActor {
 
 #[tracing::instrument(name = "cursor", skip_all)]
 pub fn spawn_cursor_recorder(
-    #[allow(unused)] screen_bounds: Bounds,
-    #[cfg(target_os = "macos")] display: cap_displays::Display,
-    #[cfg(target_os = "macos")] crop_ratio: cap_media::sources::CropRatio,
+    crop_bounds: CursorCropBounds,
+    display: cap_displays::Display,
     cursors_dir: PathBuf,
     prev_cursors: Cursors,
     next_cursor_id: u32,
     start_time: SystemTime,
 ) -> CursorActor {
+    use cap_utils::spawn_actor;
+    use device_query::{DeviceQuery, DeviceState};
+    use futures::future::Either;
     use std::{
         hash::{DefaultHasher, Hash, Hasher},
         pin::pin,
         time::Duration,
     };
-
-    use cap_utils::spawn_actor;
-    use device_query::{DeviceQuery, DeviceState};
-    use futures::future::Either;
     use tracing::{error, info};
 
     let stop_token = CancellationToken::new();
@@ -64,7 +61,6 @@ pub fn spawn_cursor_recorder(
         let device_state = DeviceState::new();
         let mut last_mouse_state = device_state.get_mouse();
 
-        #[cfg(target_os = "macos")]
         let mut last_position = cap_cursor_capture::RawCursorPosition::get();
 
         // Create cursors directory if it doesn't exist
@@ -133,67 +129,20 @@ pub fn spawn_cursor_recorder(
                 "default".to_string()
             };
 
-            // TODO: use this on windows too
-            #[cfg(target_os = "macos")]
-            let position = {
-                let position = cap_cursor_capture::RawCursorPosition::get();
-
-                if position != last_position {
-                    last_position = position;
-
-                    let cropped_position = position
-                        .relative_to_display(display)
-                        .normalize()
-                        .with_crop(crop_ratio.position, crop_ratio.size);
-
-                    Some((cropped_position.x() as f64, cropped_position.y() as f64))
-                } else {
-                    None
-                }
-            };
-
-            #[cfg(windows)]
-            let position = if mouse_state.coords != last_mouse_state.coords {
-                let (mouse_x, mouse_y) = {
-                    (
-                        mouse_state.coords.0 - screen_bounds.x as i32,
-                        mouse_state.coords.1 - screen_bounds.y as i32,
-                    )
-                };
+            let position = cap_cursor_capture::RawCursorPosition::get();
 
-                // Calculate normalized coordinates (0.0 to 1.0) within the screen bounds
-                // Check if screen_bounds dimensions are valid to avoid division by zero
-                let x = if screen_bounds.width > 0.0 {
-                    mouse_x as f64 / screen_bounds.width
-                } else {
-                    0.5 // Fallback if width is invalid
-                };
+            let position = (position != last_position).then(|| {
+                last_position = position;
 
-                let y = if screen_bounds.height > 0.0 {
-                    mouse_y as f64 / screen_bounds.height
-                } else {
-                    0.5 // Fallback if height is invalid
-                };
+                let cropped_norm_pos = position
+                    .relative_to_display(display)?
+                    .normalize()?
+                    .with_crop(crop_bounds);
 
-                // Clamp values to ensure they're within valid range
-                let x = if x.is_nan() || x.is_infinite() {
-                    0.5
-                } else {
-                    x
-                };
-
-                let y = if y.is_nan() || y.is_infinite() {
-                    0.5
-                } else {
-                    y
-                };
-
-                Some((x, y))
-            } else {
-                None
-            };
+                Some((cropped_norm_pos.x(), cropped_norm_pos.y()))
+            });
 
-            if let Some((x, y)) = position {
+            if let Some((x, y)) = position.flatten() {
                 let mouse_event = CursorMoveEvent {
                     active_modifiers: vec![],
                     cursor_id: cursor_id.clone(),
diff --git a/crates/media/src/feeds/audio_input.rs b/crates/recording/src/feeds/audio_input.rs
similarity index 100%
rename from crates/media/src/feeds/audio_input.rs
rename to crates/recording/src/feeds/audio_input.rs
diff --git a/crates/media/src/feeds/camera.rs b/crates/recording/src/feeds/camera.rs
similarity index 100%
rename from crates/media/src/feeds/camera.rs
rename to crates/recording/src/feeds/camera.rs
diff --git a/crates/media/src/feeds/mod.rs b/crates/recording/src/feeds/mod.rs
similarity index 71%
rename from crates/media/src/feeds/mod.rs
rename to crates/recording/src/feeds/mod.rs
index 7be4e726da..261dfc43ed 100644
--- a/crates/media/src/feeds/mod.rs
+++ b/crates/recording/src/feeds/mod.rs
@@ -1,7 +1,5 @@
-mod audio;
 mod audio_input;
 mod camera;
 
-pub use audio::*;
 pub use audio_input::*;
 pub use camera::*;
diff --git a/crates/recording/src/instant_recording.rs b/crates/recording/src/instant_recording.rs
index eb90c59a9e..9833fb9c7b 100644
--- a/crates/recording/src/instant_recording.rs
+++ b/crates/recording/src/instant_recording.rs
@@ -1,30 +1,26 @@
+use cap_media::MediaError;
+use cap_media_info::VideoInfo;
+use cap_project::InstantRecordingMeta;
+use cap_utils::{ensure_dir, spawn_actor};
+use flume::Receiver;
 use std::{
     path::PathBuf,
     sync::{Arc, atomic::AtomicBool},
     time::{SystemTime, UNIX_EPOCH},
 };
-
-use cap_media::{
-    MediaError,
-    feeds::AudioInputFeed,
-    pipeline::{Pipeline, RealTimeClock},
-    platform::Bounds,
-    sources::{ScreenCaptureSource, ScreenCaptureTarget},
-};
-use cap_media_info::VideoInfo;
-use cap_project::InstantRecordingMeta;
-use cap_utils::{ensure_dir, spawn_actor};
-use flume::Receiver;
 use tokio::sync::oneshot;
 use tracing::{Instrument, debug, error, info, trace};
 
 use crate::{
     ActorError, RecordingBaseInputs, RecordingError,
     capture_pipeline::{MakeCapturePipeline, create_screen_capture},
+    feeds::AudioInputFeed,
+    pipeline::Pipeline,
+    sources::{ScreenCaptureSource, ScreenCaptureTarget},
 };
 
 struct InstantRecordingPipeline {
-    pub inner: Pipeline<RealTimeClock<()>>,
+    pub inner: Pipeline,
     #[allow(unused)]
     pub output_path: PathBuf,
     pub pause_flag: Arc<AtomicBool>,
@@ -47,7 +43,7 @@ enum InstantRecordingActorState {
 pub struct InstantRecordingHandle {
     ctrl_tx: flume::Sender<InstantRecordingActorControlMessage>,
     pub capture_target: ScreenCaptureTarget,
-    pub bounds: Bounds,
+    // pub bounds: Bounds,
 }
 
 macro_rules! send_message {
@@ -127,8 +123,7 @@ async fn create_pipeline<TCaptureFormat: MakeCapturePipeline>(
     ),
     MediaError,
 > {
-    let clock = RealTimeClock::<()>::new();
-    let pipeline_builder = Pipeline::builder(clock);
+    let pipeline_builder = Pipeline::builder();
 
     let pause_flag = Arc::new(AtomicBool::new(false));
     let system_audio = system_audio.map(|v| (v, screen_source.0.audio_info()));
@@ -184,15 +179,8 @@ pub async fn spawn_instant_recording_actor<'a>(
         (None, None)
     };
 
-    let (screen_source, screen_rx) = create_screen_capture(
-        &inputs.capture_target,
-        true,
-        true,
-        30,
-        system_audio.0,
-        start_time,
-    )
-    .await?;
+    let (screen_source, screen_rx) =
+        create_screen_capture(&inputs.capture_target, true, 30, system_audio.0, start_time).await?;
 
     debug!("screen capture: {screen_source:#?}");
 
@@ -253,7 +241,7 @@ pub async fn spawn_instant_recording_actor<'a>(
         InstantRecordingHandle {
             ctrl_tx,
             capture_target: inputs.capture_target,
-            bounds: *screen_source.get_bounds(),
+            // bounds: *screen_source.get_bounds(),
         },
         done_rx,
     ))
diff --git a/crates/recording/src/lib.rs b/crates/recording/src/lib.rs
index e4258a43ce..1c615552e9 100644
--- a/crates/recording/src/lib.rs
+++ b/crates/recording/src/lib.rs
@@ -1,13 +1,21 @@
 mod capture_pipeline;
 pub mod cursor;
+pub mod feeds;
 pub mod instant_recording;
+pub mod pipeline;
+pub mod sources;
 pub mod studio_recording;
 
+pub use sources::{camera, screen_capture};
 pub use studio_recording::{
     CompletedStudioRecording, StudioRecordingHandle, spawn_studio_recording_actor,
 };
 
-use cap_media::{MediaError, feeds::AudioInputFeed, platform::Bounds, sources::*};
+use feeds::AudioInputFeed;
+use sources::*;
+
+use cap_displays::bounds::LogicalBounds;
+use cap_media::MediaError;
 use serde::{Deserialize, Serialize};
 use thiserror::Error;
 
@@ -40,9 +48,16 @@ pub struct RecordingBaseInputs<'a> {
 #[derive(specta::Type, Serialize, Deserialize, Clone, Debug)]
 #[serde(rename_all = "camelCase")]
 pub enum RecordingOptionCaptureTarget {
-    Window { id: u32 },
-    Screen { id: u32 },
-    Area { screen_id: u32, bounds: Bounds },
+    Window {
+        id: u32,
+    },
+    Screen {
+        id: u32,
+    },
+    Area {
+        screen_id: u32,
+        bounds: LogicalBounds,
+    },
 }
 
 impl RecordingOptions {
diff --git a/crates/media/src/pipeline/audio_buffer.rs b/crates/recording/src/pipeline/audio_buffer.rs
similarity index 98%
rename from crates/media/src/pipeline/audio_buffer.rs
rename to crates/recording/src/pipeline/audio_buffer.rs
index ececf2b51d..9b85ced02e 100644
--- a/crates/media/src/pipeline/audio_buffer.rs
+++ b/crates/recording/src/pipeline/audio_buffer.rs
@@ -1,10 +1,9 @@
-use std::collections::VecDeque;
+use cap_audio::cast_bytes_to_f32_slice;
 
 use cap_media_info::{AudioInfo, PlanarData};
 use ffmpeg::encoder;
 pub use ffmpeg::util::frame::Audio as FFAudio;
-
-use crate::data::cast_bytes_to_f32_slice;
+use std::collections::VecDeque;
 
 #[derive(Debug)]
 pub struct AudioBuffer {
diff --git a/crates/media/src/pipeline/builder.rs b/crates/recording/src/pipeline/builder.rs
similarity index 82%
rename from crates/media/src/pipeline/builder.rs
rename to crates/recording/src/pipeline/builder.rs
index d878ff96c5..6bcba23daf 100644
--- a/crates/media/src/pipeline/builder.rs
+++ b/crates/recording/src/pipeline/builder.rs
@@ -8,8 +8,7 @@ use tokio::sync::oneshot;
 use tracing::{error, info};
 
 use crate::pipeline::{
-    MediaError, Pipeline, PipelineClock,
-    clock::CloneFrom,
+    MediaError, Pipeline,
     control::ControlBroadcast,
     task::{PipelineReadySignal, PipelineSourceTask},
 };
@@ -20,32 +19,37 @@ struct Task {
     done_rx: tokio::sync::oneshot::Receiver<Result<(), String>>,
 }
 
-pub struct PipelineBuilder<T> {
-    clock: T,
+pub struct PipelineBuilder {
     control: ControlBroadcast,
     tasks: IndexMap<String, Task>,
 }
 
-impl<T> PipelineBuilder<T> {
-    pub fn new(clock: T) -> Self {
+impl PipelineBuilder {
+    pub fn new() -> Self {
         Self {
-            clock,
             control: ControlBroadcast::default(),
             tasks: IndexMap::new(),
         }
     }
 
-    pub fn spawn_source<C: CloneFrom<T> + Send + 'static>(
+    pub fn spawn_source(
         &mut self,
         name: impl Into<String>,
-        mut task: impl PipelineSourceTask<Clock = C> + 'static,
+        mut task: impl PipelineSourceTask + 'static,
     ) {
         let name = name.into();
-        let clock = C::clone_from(&self.clock);
         let control_signal = self.control.add_listener(name.clone());
 
-        self.spawn_task(name, move |ready_signal| {
-            task.run(clock, ready_signal, control_signal)
+        self.spawn_task(name.clone(), move |ready_signal| {
+            let res = task.run(ready_signal.clone(), control_signal);
+
+            if let Err(e) = &res
+                && !ready_signal.is_disconnected()
+            {
+                let _ = ready_signal.send(Err(MediaError::Any(format!("Task/{name}/{e}").into())));
+            }
+
+            res
         });
     }
 
@@ -106,15 +110,11 @@ impl<T> PipelineBuilder<T> {
     }
 }
 
-impl<T: PipelineClock> PipelineBuilder<T> {
+impl PipelineBuilder {
     pub async fn build(
         self,
-    ) -> Result<(Pipeline<T>, oneshot::Receiver<Result<(), String>>), MediaError> {
-        let Self {
-            clock,
-            control,
-            tasks,
-        } = self;
+    ) -> Result<(Pipeline, oneshot::Receiver<Result<(), String>>), MediaError> {
+        let Self { control, tasks } = self;
 
         if tasks.is_empty() {
             return Err(MediaError::EmptyPipeline);
@@ -131,7 +131,7 @@ impl<T: PipelineClock> PipelineBuilder<T> {
             tokio::time::timeout(Duration::from_secs(5), task.ready_signal.recv_async())
                 .await
                 .map_err(|_| MediaError::TaskLaunch(format!("task timed out: '{name}'")))?
-                .map_err(|e| MediaError::TaskLaunch(format!("{name} build / {e}")))??;
+                .map_err(|e| MediaError::TaskLaunch(format!("'{name}' build / {e}")))??;
 
             task_handles.insert(name.clone(), task.join_handle);
             stop_rx.push(task.done_rx);
@@ -147,8 +147,8 @@ impl<T: PipelineClock> PipelineBuilder<T> {
             let task_name = &task_names[index];
 
             let result = match result {
-                Ok(Err(error)) => Err(format!("Task '{task_name}' failed: {error}")),
-                Err(_) => Err(format!("Task '{task_name}' failed for unknown reason")),
+                Ok(Err(error)) => Err(format!("Task/{task_name}/{error}")),
+                Err(_) => Err(format!("Task/{task_name}/Unknown")),
                 _ => Ok(()),
             };
 
@@ -161,7 +161,6 @@ impl<T: PipelineClock> PipelineBuilder<T> {
 
         Ok((
             Pipeline {
-                clock,
                 control,
                 task_handles,
                 is_shutdown: false,
diff --git a/crates/media/src/pipeline/control.rs b/crates/recording/src/pipeline/control.rs
similarity index 92%
rename from crates/media/src/pipeline/control.rs
rename to crates/recording/src/pipeline/control.rs
index 39aa72ab17..8fabc57d44 100644
--- a/crates/media/src/pipeline/control.rs
+++ b/crates/recording/src/pipeline/control.rs
@@ -8,12 +8,21 @@ pub enum Control {
     Shutdown,
 }
 
+#[derive(Clone)]
 pub struct PipelineControlSignal {
     last_value: Option<Control>,
     receiver: Receiver<Control>,
 }
 
 impl PipelineControlSignal {
+    pub fn receiver(&self) -> &Receiver<Control> {
+        &self.receiver
+    }
+
+    pub fn last_cached(&self) -> Option<Control> {
+        self.last_value
+    }
+
     pub fn last(&mut self) -> Option<Control> {
         self.blocking_last_if(false)
     }
diff --git a/crates/media/src/pipeline/mod.rs b/crates/recording/src/pipeline/mod.rs
similarity index 81%
rename from crates/media/src/pipeline/mod.rs
rename to crates/recording/src/pipeline/mod.rs
index decc07834f..4ba97466e0 100644
--- a/crates/media/src/pipeline/mod.rs
+++ b/crates/recording/src/pipeline/mod.rs
@@ -4,26 +4,23 @@ use tracing::{info, trace};
 
 pub mod audio_buffer;
 pub mod builder;
-pub mod clock;
 pub mod control;
 pub mod task;
 
 use crate::MediaError;
 
 use builder::PipelineBuilder;
-pub use clock::*;
 use control::{Control, ControlBroadcast, PipelineControlSignal};
 
-pub struct Pipeline<T: PipelineClock> {
-    clock: T,
+pub struct Pipeline {
     control: ControlBroadcast,
     task_handles: IndexMap<String, JoinHandle<()>>,
     is_shutdown: bool,
 }
 
-impl<T: PipelineClock> Pipeline<T> {
-    pub fn builder(clock: T) -> PipelineBuilder<T> {
-        PipelineBuilder::new(clock)
+impl Pipeline {
+    pub fn builder() -> PipelineBuilder {
+        PipelineBuilder::new()
     }
 
     pub async fn play(&mut self) -> Result<(), MediaError> {
@@ -31,7 +28,6 @@ impl<T: PipelineClock> Pipeline<T> {
             return Err(MediaError::ShutdownPipeline);
         };
 
-        self.clock.start();
         self.control.broadcast(Control::Play).await;
 
         Ok(())
diff --git a/crates/media/src/pipeline/task.rs b/crates/recording/src/pipeline/task.rs
similarity index 90%
rename from crates/media/src/pipeline/task.rs
rename to crates/recording/src/pipeline/task.rs
index b669d52104..0776143ebd 100644
--- a/crates/media/src/pipeline/task.rs
+++ b/crates/recording/src/pipeline/task.rs
@@ -7,11 +7,8 @@ const DEFAULT_QUEUE_SIZE: usize = 2048;
 pub type PipelineReadySignal = Sender<Result<(), MediaError>>;
 
 pub trait PipelineSourceTask: Send {
-    type Clock;
-
     fn run(
         &mut self,
-        clock: Self::Clock,
         ready_signal: PipelineReadySignal,
         control_signal: PipelineControlSignal,
     ) -> Result<(), String>;
diff --git a/crates/media/src/sources/audio_input.rs b/crates/recording/src/sources/audio_input.rs
similarity index 88%
rename from crates/media/src/sources/audio_input.rs
rename to crates/recording/src/sources/audio_input.rs
index 9cfec73fd4..677414ae20 100644
--- a/crates/media/src/sources/audio_input.rs
+++ b/crates/recording/src/sources/audio_input.rs
@@ -1,32 +1,20 @@
-use std::time::SystemTime;
-
 use cap_fail::fail;
+use cap_media::MediaError;
 use cap_media_info::AudioInfo;
 use cpal::{Device, StreamInstant, SupportedStreamConfig};
-use ffmpeg_sys_next::AV_TIME_BASE_Q;
+use ffmpeg::{frame::Audio as FFAudio, sys::AV_TIME_BASE_Q};
 use flume::{Receiver, Sender};
 use indexmap::IndexMap;
+use std::time::SystemTime;
 use tracing::{error, info};
 
-use crate::feeds::{AudioInputConnection, AudioInputFeed, AudioInputSamples};
 use crate::{
-    MediaError,
-    data::FFAudio,
-    pipeline::{
-        clock::{LocalTimestamp, RealTimeClock},
-        control::Control,
-        task::PipelineSourceTask,
-    },
+    feeds::{AudioInputConnection, AudioInputFeed, AudioInputSamples},
+    pipeline::{control::Control, task::PipelineSourceTask},
 };
 
 pub type AudioInputDeviceMap = IndexMap<String, (Device, SupportedStreamConfig)>;
 
-impl LocalTimestamp for StreamInstant {
-    fn elapsed_since(&self, other: &Self) -> std::time::Duration {
-        self.duration_since(other).unwrap()
-    }
-}
-
 pub struct AudioInputSource {
     feed_connection: AudioInputConnection,
     audio_info: AudioInfo,
@@ -103,11 +91,8 @@ impl AudioInputSource {
 }
 
 impl PipelineSourceTask for AudioInputSource {
-    type Clock = RealTimeClock<StreamInstant>;
-
     fn run(
         &mut self,
-        _: Self::Clock,
         ready_signal: crate::pipeline::task::PipelineReadySignal,
         mut control_signal: crate::pipeline::control::PipelineControlSignal,
     ) -> Result<(), String> {
diff --git a/crates/media/src/sources/audio_mixer.rs b/crates/recording/src/sources/audio_mixer.rs
similarity index 96%
rename from crates/media/src/sources/audio_mixer.rs
rename to crates/recording/src/sources/audio_mixer.rs
index 52d26ecbf5..57f8d509ea 100644
--- a/crates/media/src/sources/audio_mixer.rs
+++ b/crates/recording/src/sources/audio_mixer.rs
@@ -1,11 +1,10 @@
-use std::time::Duration;
-
 use cap_media_info::AudioInfo;
-use ffmpeg_sys_next::AV_TIME_BASE_Q;
+use ffmpeg::sys::AV_TIME_BASE_Q;
 use flume::{Receiver, Sender};
+use std::time::Duration;
 use tracing::{debug, warn};
 
-use crate::pipeline::{RawNanoseconds, RealTimeClock, task::PipelineSourceTask};
+use crate::pipeline::task::PipelineSourceTask;
 
 pub struct AudioMixer {
     sources: Vec<AudioMixerSource>,
@@ -162,11 +161,8 @@ pub struct AudioMixerSource {
 }
 
 impl PipelineSourceTask for AudioMixer {
-    type Clock = RealTimeClock<RawNanoseconds>;
-
     fn run(
         &mut self,
-        _clock: Self::Clock,
         ready_signal: crate::pipeline::task::PipelineReadySignal,
         mut control_signal: crate::pipeline::control::PipelineControlSignal,
     ) -> Result<(), String> {
diff --git a/crates/media/src/sources/camera.rs b/crates/recording/src/sources/camera.rs
similarity index 96%
rename from crates/media/src/sources/camera.rs
rename to crates/recording/src/sources/camera.rs
index 12c5d404be..d1e49b32b2 100644
--- a/crates/media/src/sources/camera.rs
+++ b/crates/recording/src/sources/camera.rs
@@ -7,7 +7,7 @@ use tracing::{error, info};
 use crate::{
     MediaError,
     feeds::{CameraConnection, CameraFeed, RawCameraFrame},
-    pipeline::{clock::RealTimeClock, control::Control, task::PipelineSourceTask},
+    pipeline::{control::Control, task::PipelineSourceTask},
 };
 
 pub struct CameraSource {
@@ -92,12 +92,9 @@ impl CameraSource {
 }
 
 impl PipelineSourceTask for CameraSource {
-    type Clock = RealTimeClock<Instant>;
-
     // #[tracing::instrument(skip_all)]
     fn run(
         &mut self,
-        _: Self::Clock,
         ready_signal: crate::pipeline::task::PipelineReadySignal,
         mut control_signal: crate::pipeline::control::PipelineControlSignal,
     ) -> Result<(), String> {
diff --git a/crates/media/src/sources/mod.rs b/crates/recording/src/sources/mod.rs
similarity index 51%
rename from crates/media/src/sources/mod.rs
rename to crates/recording/src/sources/mod.rs
index cdf7f3ca61..d83b759113 100644
--- a/crates/media/src/sources/mod.rs
+++ b/crates/recording/src/sources/mod.rs
@@ -1,8 +1,7 @@
-mod audio_input;
-mod audio_mixer;
-mod camera;
-mod screen_capture;
-// pub mod system_audio;
+pub mod audio_input;
+pub mod audio_mixer;
+pub mod camera;
+pub mod screen_capture;
 
 pub use audio_input::*;
 pub use audio_mixer::*;
diff --git a/crates/recording/src/sources/screen_capture/macos.rs b/crates/recording/src/sources/screen_capture/macos.rs
new file mode 100644
index 0000000000..cd41780b4b
--- /dev/null
+++ b/crates/recording/src/sources/screen_capture/macos.rs
@@ -0,0 +1,370 @@
+use super::*;
+use cidre::*;
+use kameo::prelude::*;
+
+#[derive(Debug)]
+pub struct CMSampleBufferCapture;
+
+impl ScreenCaptureFormat for CMSampleBufferCapture {
+    type VideoFormat = cidre::arc::R<cidre::cm::SampleBuf>;
+
+    fn pixel_format() -> ffmpeg::format::Pixel {
+        ffmpeg::format::Pixel::BGRA
+    }
+
+    fn audio_info() -> AudioInfo {
+        AudioInfo::new(
+            ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar),
+            48_000,
+            2,
+        )
+        .unwrap()
+    }
+}
+
+#[derive(Actor)]
+struct FrameHandler {
+    start_time_unix: f64,
+    start_cmtime: f64,
+    start_time_f64: f64,
+    video_tx: Sender<(arc::R<cm::SampleBuf>, f64)>,
+    audio_tx: Option<Sender<(ffmpeg::frame::Audio, f64)>>,
+}
+
+impl Message<NewFrame> for FrameHandler {
+    type Reply = ();
+
+    async fn handle(
+        &mut self,
+        msg: NewFrame,
+        _: &mut kameo::prelude::Context<Self, Self::Reply>,
+    ) -> Self::Reply {
+        let frame = msg.0;
+        let sample_buffer = frame.sample_buf();
+
+        let frame_time = sample_buffer.pts().value as f64 / sample_buffer.pts().scale as f64;
+        let unix_timestamp = self.start_time_unix + frame_time - self.start_cmtime;
+        let relative_time = unix_timestamp - self.start_time_f64;
+
+        match &frame {
+            scap_screencapturekit::Frame::Screen(frame) => {
+                if frame.image_buf().height() == 0 || frame.image_buf().width() == 0 {
+                    return;
+                }
+
+                let check_skip_send = || {
+                    cap_fail::fail_err!("media::sources::screen_capture::skip_send", ());
+
+                    Ok::<(), ()>(())
+                };
+
+                if check_skip_send().is_ok()
+                    && self
+                        .video_tx
+                        .send((sample_buffer.retained(), relative_time))
+                        .is_err()
+                {
+                    warn!("Pipeline is unreachable");
+                }
+            }
+            scap_screencapturekit::Frame::Audio(_) => {
+                use ffmpeg::ChannelLayout;
+
+                let res = || {
+                    cap_fail::fail_err!("screen_capture audio skip", ());
+                    Ok::<(), ()>(())
+                };
+                if res().is_err() {
+                    return;
+                }
+
+                let Some(audio_tx) = &self.audio_tx else {
+                    return;
+                };
+
+                let buf_list = sample_buffer.audio_buf_list::<2>().unwrap();
+                let slice = buf_list.block().as_slice().unwrap();
+
+                let mut frame = ffmpeg::frame::Audio::new(
+                    ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar),
+                    sample_buffer.num_samples() as usize,
+                    ChannelLayout::STEREO,
+                );
+                frame.set_rate(48_000);
+                let data_bytes_size = buf_list.list().buffers[0].data_bytes_size;
+                for i in 0..frame.planes() {
+                    use cap_media_info::PlanarData;
+
+                    frame.plane_data_mut(i).copy_from_slice(
+                        &slice[i * data_bytes_size as usize..(i + 1) * data_bytes_size as usize],
+                    );
+                }
+
+                frame.set_pts(Some((relative_time * AV_TIME_BASE_Q.den as f64) as i64));
+
+                let _ = audio_tx.send((frame, relative_time));
+            }
+            _ => {}
+        }
+    }
+}
+
+#[derive(Debug, thiserror::Error)]
+enum SourceError {
+    #[error("NoDisplay: Id '{0}'")]
+    NoDisplay(DisplayId),
+    #[error("AsContentFilter")]
+    AsContentFilter,
+    #[error("CreateActor: {0}")]
+    CreateActor(arc::R<ns::Error>),
+    #[error("StartCapturing/{0}")]
+    StartCapturing(SendError<StartCapturing, StartCapturingError>),
+    #[error("DidStopWithError: {0}")]
+    DidStopWithError(arc::R<ns::Error>),
+}
+
+impl PipelineSourceTask for ScreenCaptureSource<CMSampleBufferCapture> {
+    fn run(
+        &mut self,
+        ready_signal: crate::pipeline::task::PipelineReadySignal,
+        control_signal: crate::pipeline::control::PipelineControlSignal,
+    ) -> Result<(), String> {
+        let start = std::time::SystemTime::now();
+        let start_time_unix = start
+            .duration_since(std::time::UNIX_EPOCH)
+            .expect("Time went backwards")
+            .as_secs_f64();
+        let start_cmtime = cidre::cm::Clock::host_time_clock().time();
+        let start_cmtime = start_cmtime.value as f64 / start_cmtime.scale as f64;
+
+        let start_time_f64 = self
+            .start_time
+            .duration_since(SystemTime::UNIX_EPOCH)
+            .unwrap()
+            .as_secs_f64();
+
+        let video_tx = self.video_tx.clone();
+        let audio_tx = self.audio_tx.clone();
+        let config = self.config.clone();
+
+        self.tokio_handle
+            .block_on(async move {
+                let captures_audio = audio_tx.is_some();
+                let frame_handler = FrameHandler::spawn(FrameHandler {
+                    video_tx,
+                    audio_tx,
+                    start_time_unix,
+                    start_cmtime,
+                    start_time_f64,
+                });
+
+                let display = Display::from_id(&config.display)
+                    .ok_or_else(|| SourceError::NoDisplay(config.display))?;
+
+                let content_filter = display
+                    .raw_handle()
+                    .as_content_filter()
+                    .await
+                    .ok_or_else(|| SourceError::AsContentFilter)?;
+
+                let size = {
+                    let logical_size = config
+                        .crop_bounds
+                        .map(|bounds| bounds.size())
+                        .or_else(|| display.logical_size())
+                        .unwrap();
+
+                    let scale = display.physical_size().unwrap().width()
+                        / display.logical_size().unwrap().width();
+
+                    PhysicalSize::new(logical_size.width() * scale, logical_size.height() * scale)
+                };
+
+                tracing::info!("size: {:?}", size);
+
+                let mut settings = scap_screencapturekit::StreamCfgBuilder::default()
+                    .with_width(size.width() as usize)
+                    .with_height(size.height() as usize)
+                    .with_fps(config.fps as f32)
+                    .with_shows_cursor(config.show_cursor)
+                    .with_captures_audio(captures_audio)
+                    .build();
+
+                settings.set_pixel_format(cv::PixelFormat::_32_BGRA);
+
+                if let Some(crop_bounds) = config.crop_bounds {
+                    tracing::info!("crop bounds: {:?}", crop_bounds);
+                    settings.set_src_rect(cg::Rect::new(
+                        crop_bounds.position().x(),
+                        crop_bounds.position().y(),
+                        crop_bounds.size().width(),
+                        crop_bounds.size().height(),
+                    ));
+                }
+
+                let (error_tx, error_rx) = flume::bounded(1);
+
+                let capturer = ScreenCaptureActor::spawn(
+                    ScreenCaptureActor::new(
+                        content_filter,
+                        settings,
+                        frame_handler.recipient(),
+                        error_tx.clone(),
+                    )
+                    .map_err(SourceError::CreateActor)?,
+                );
+
+                let stop_recipient = capturer.clone().reply_recipient::<StopCapturing>();
+
+                let _ = capturer
+                    .ask(StartCapturing)
+                    .await
+                    .map_err(SourceError::StartCapturing)?;
+
+                let _ = ready_signal.send(Ok(()));
+
+                let stop = async move {
+                    let _ = capturer.ask(StopCapturing).await;
+                    let _ = capturer.stop_gracefully().await;
+                };
+
+                loop {
+                    use futures::future::Either;
+
+                    match futures::future::select(
+                        error_rx.recv_async(),
+                        control_signal.receiver().recv_async(),
+                    )
+                    .await
+                    {
+                        Either::Left((Ok(error), _)) => {
+                            error!("Error capturing screen: {}", error);
+                            stop.await;
+                            return Err(SourceError::DidStopWithError(error));
+                        }
+                        Either::Right((Ok(ctrl), _)) => {
+                            if let Control::Shutdown = ctrl {
+                                stop.await;
+                                return Ok(());
+                            }
+                        }
+                        _ => {
+                            warn!("Screen capture recv channels shutdown, exiting.");
+
+                            stop.await;
+
+                            return Ok(());
+                        }
+                    }
+                }
+            })
+            .map_err(|e: SourceError| e.to_string())
+    }
+}
+
+#[derive(Actor)]
+pub struct ScreenCaptureActor {
+    capturer: scap_screencapturekit::Capturer,
+    capturing: bool,
+}
+
+impl ScreenCaptureActor {
+    pub fn new(
+        target: arc::R<sc::ContentFilter>,
+        settings: arc::R<sc::StreamCfg>,
+        frame_handler: Recipient<NewFrame>,
+        error_tx: Sender<arc::R<ns::Error>>,
+    ) -> Result<Self, arc::R<ns::Error>> {
+        cap_fail::fail_err!(
+            "macos::ScreenCaptureActor::new",
+            ns::Error::with_domain(ns::ErrorDomain::os_status(), 69420, None)
+        );
+
+        let _error_tx = error_tx.clone();
+        let capturer_builder = scap_screencapturekit::Capturer::builder(target, settings)
+            .with_output_sample_buf_cb(move |frame| {
+                let check_err = || {
+                    Result::<_, arc::R<ns::Error>>::Ok(cap_fail::fail_err!(
+                        "macos::ScreenCaptureActor output_sample_buf",
+                        ns::Error::with_domain(ns::ErrorDomain::os_status(), 69420, None)
+                    ))
+                };
+                if let Err(e) = check_err() {
+                    let _ = _error_tx.send(e);
+                }
+
+                let _ = frame_handler.tell(NewFrame(frame)).try_send();
+            })
+            .with_stop_with_err_cb(move |_, err| {
+                let _ = error_tx.send(err.retained());
+            });
+
+        Ok(ScreenCaptureActor {
+            capturer: capturer_builder.build()?,
+            capturing: false,
+        })
+    }
+}
+
+// Public
+
+pub struct StartCapturing;
+
+// External
+
+pub struct NewFrame(pub scap_screencapturekit::Frame);
+
+// Internal
+
+pub struct CaptureError(pub arc::R<ns::Error>);
+
+#[derive(Debug, Clone, thiserror::Error)]
+pub enum StartCapturingError {
+    #[error("AlreadyCapturing")]
+    AlreadyCapturing,
+    #[error("Start: {0}")]
+    Start(arc::R<ns::Error>),
+}
+
+impl Message<StartCapturing> for ScreenCaptureActor {
+    type Reply = Result<(), StartCapturingError>;
+
+    async fn handle(
+        &mut self,
+        _: StartCapturing,
+        _: &mut Context<Self, Self::Reply>,
+    ) -> Self::Reply {
+        if self.capturing {
+            return Err(StartCapturingError::AlreadyCapturing);
+        }
+
+        self.capturer
+            .start()
+            .await
+            .map_err(StartCapturingError::Start)?;
+
+        self.capturing = true;
+
+        Ok(())
+    }
+}
+
+impl Message<StopCapturing> for ScreenCaptureActor {
+    type Reply = Result<(), StopCapturingError>;
+
+    async fn handle(
+        &mut self,
+        _: StopCapturing,
+        _: &mut Context<Self, Self::Reply>,
+    ) -> Self::Reply {
+        if !self.capturing {
+            return Err(StopCapturingError::NotCapturing);
+        };
+
+        if let Err(e) = self.capturer.stop().await {
+            error!("Silently failed to stop macOS capturer: {}", e);
+        }
+
+        Ok(())
+    }
+}
diff --git a/crates/recording/src/sources/screen_capture/mod.rs b/crates/recording/src/sources/screen_capture/mod.rs
new file mode 100644
index 0000000000..34a1a66d41
--- /dev/null
+++ b/crates/recording/src/sources/screen_capture/mod.rs
@@ -0,0 +1,458 @@
+use cap_cursor_capture::CursorCropBounds;
+use cap_displays::{Display, DisplayId, Window, WindowId, bounds::*};
+use cap_media_info::{AudioInfo, VideoInfo};
+use ffmpeg::sys::AV_TIME_BASE_Q;
+use flume::Sender;
+use serde::{Deserialize, Serialize};
+use specta::Type;
+use std::time::SystemTime;
+use tracing::{error, warn};
+
+use crate::pipeline::{control::Control, task::PipelineSourceTask};
+
+#[cfg(windows)]
+mod windows;
+#[cfg(windows)]
+pub use windows::*;
+
+#[cfg(target_os = "macos")]
+mod macos;
+#[cfg(target_os = "macos")]
+pub use macos::*;
+
+pub struct StopCapturing;
+
+#[derive(Debug, Clone)]
+pub enum StopCapturingError {
+    NotCapturing,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Type)]
+pub struct CaptureWindow {
+    pub id: WindowId,
+    pub owner_name: String,
+    pub name: String,
+    pub bounds: LogicalBounds,
+    pub refresh_rate: u32,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Type)]
+pub struct CaptureDisplay {
+    pub id: DisplayId,
+    pub name: String,
+    pub refresh_rate: u32,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Type)]
+pub struct CaptureArea {
+    pub screen: CaptureDisplay,
+    pub bounds: LogicalBounds,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Type)]
+#[serde(rename_all = "camelCase", tag = "variant")]
+pub enum ScreenCaptureTarget {
+    Window {
+        id: WindowId,
+    },
+    Display {
+        id: DisplayId,
+    },
+    Area {
+        screen: DisplayId,
+        bounds: LogicalBounds,
+    },
+}
+
+impl ScreenCaptureTarget {
+    pub fn display(&self) -> Option<Display> {
+        match self {
+            Self::Display { id } => Display::from_id(id),
+            Self::Window { id } => Window::from_id(id).and_then(|w| w.display()),
+            Self::Area { screen, .. } => Display::from_id(screen),
+        }
+    }
+
+    pub fn cursor_crop(&self) -> Option<CursorCropBounds> {
+        match self {
+            Self::Display { .. } => {
+                #[cfg(target_os = "macos")]
+                {
+                    let display = self.display()?;
+                    return Some(CursorCropBounds::new_macos(LogicalBounds::new(
+                        LogicalPosition::new(0.0, 0.0),
+                        display.raw_handle().logical_size()?,
+                    )));
+                }
+
+                #[cfg(windows)]
+                {
+                    let display = self.display()?;
+                    return Some(CursorCropBounds::new_windows(PhysicalBounds::new(
+                        PhysicalPosition::new(0.0, 0.0),
+                        display.raw_handle().physical_size()?,
+                    )));
+                }
+            }
+            Self::Window { id } => {
+                let window = Window::from_id(id)?;
+
+                #[cfg(target_os = "macos")]
+                {
+                    let display = self.display()?;
+                    let display_position = display.raw_handle().logical_position();
+                    let window_bounds = window.raw_handle().logical_bounds()?;
+
+                    return Some(CursorCropBounds::new_macos(LogicalBounds::new(
+                        LogicalPosition::new(
+                            window_bounds.position().x() - display_position.x(),
+                            window_bounds.position().y() - display_position.y(),
+                        ),
+                        window_bounds.size(),
+                    )));
+                }
+
+                #[cfg(windows)]
+                {
+                    let display_bounds = self.display()?.raw_handle().physical_bounds()?;
+                    let window_bounds = window.raw_handle().physical_bounds()?;
+
+                    return Some(CursorCropBounds::new_windows(PhysicalBounds::new(
+                        PhysicalPosition::new(
+                            window_bounds.position().x() - display_bounds.position().x(),
+                            window_bounds.position().y() - display_bounds.position().y(),
+                        ),
+                        PhysicalSize::new(
+                            window_bounds.size().width(),
+                            window_bounds.size().height(),
+                        ),
+                    )));
+                }
+            }
+            Self::Area { bounds, .. } => {
+                #[cfg(target_os = "macos")]
+                {
+                    return Some(CursorCropBounds::new_macos(*bounds));
+                }
+
+                #[cfg(windows)]
+                {
+                    let display = self.display()?;
+                    let display_bounds = display.raw_handle().physical_bounds()?;
+                    let display_logical_size = display.logical_size()?;
+
+                    let scale = display_bounds.size().width() / display_logical_size.width();
+
+                    return Some(CursorCropBounds::new_windows(PhysicalBounds::new(
+                        PhysicalPosition::new(
+                            bounds.position().x() * scale,
+                            bounds.position().y() * scale,
+                        ),
+                        PhysicalSize::new(
+                            bounds.size().width() * scale,
+                            bounds.size().height() * scale,
+                        ),
+                    )));
+                }
+            }
+        }
+    }
+
+    pub fn physical_size(&self) -> Option<PhysicalSize> {
+        match self {
+            Self::Display { id } => Display::from_id(id).and_then(|d| d.physical_size()),
+            Self::Window { id } => Window::from_id(id).and_then(|w| w.physical_size()),
+            Self::Area { bounds, .. } => {
+                let display = self.display()?;
+                let scale = display.physical_size()?.width() / display.logical_size()?.width();
+                let size = bounds.size();
+
+                Some(PhysicalSize::new(
+                    size.width() * scale,
+                    size.height() * scale,
+                ))
+            }
+        }
+    }
+
+    pub fn title(&self) -> Option<String> {
+        match self {
+            Self::Display { id } => Display::from_id(id).and_then(|d| d.name()),
+            Self::Window { id } => Window::from_id(id).and_then(|w| w.name()),
+            Self::Area { screen, .. } => Display::from_id(screen).and_then(|d| d.name()),
+        }
+    }
+}
+
+pub struct ScreenCaptureSource<TCaptureFormat: ScreenCaptureFormat> {
+    config: Config,
+    video_info: VideoInfo,
+    tokio_handle: tokio::runtime::Handle,
+    video_tx: Sender<(TCaptureFormat::VideoFormat, f64)>,
+    audio_tx: Option<Sender<(ffmpeg::frame::Audio, f64)>>,
+    start_time: SystemTime,
+    _phantom: std::marker::PhantomData<TCaptureFormat>,
+}
+
+impl<T: ScreenCaptureFormat> std::fmt::Debug for ScreenCaptureSource<T> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("ScreenCaptureSource")
+            // .field("bounds", &self.bounds)
+            // .field("output_resolution", &self.output_resolution)
+            .field("fps", &self.config.fps)
+            .field("video_info", &self.video_info)
+            .field(
+                "audio_info",
+                &self.audio_tx.as_ref().map(|_| self.audio_info()),
+            )
+            .finish()
+    }
+}
+
+unsafe impl<T: ScreenCaptureFormat> Send for ScreenCaptureSource<T> {}
+unsafe impl<T: ScreenCaptureFormat> Sync for ScreenCaptureSource<T> {}
+
+pub trait ScreenCaptureFormat {
+    type VideoFormat;
+
+    fn pixel_format() -> ffmpeg::format::Pixel;
+
+    fn audio_info() -> AudioInfo;
+}
+
+impl<TCaptureFormat: ScreenCaptureFormat> Clone for ScreenCaptureSource<TCaptureFormat> {
+    fn clone(&self) -> Self {
+        Self {
+            config: self.config.clone(),
+            video_info: self.video_info,
+            video_tx: self.video_tx.clone(),
+            audio_tx: self.audio_tx.clone(),
+            tokio_handle: self.tokio_handle.clone(),
+            start_time: self.start_time,
+            _phantom: std::marker::PhantomData,
+        }
+    }
+}
+
+#[derive(Clone, Debug)]
+struct Config {
+    display: DisplayId,
+    #[cfg(windows)]
+    crop_bounds: Option<PhysicalBounds>,
+    #[cfg(target_os = "macos")]
+    crop_bounds: Option<LogicalBounds>,
+    fps: u32,
+    show_cursor: bool,
+}
+
+#[derive(Debug, Clone, thiserror::Error)]
+pub enum ScreenCaptureInitError {
+    #[error("NoDisplay")]
+    NoDisplay,
+    #[error("NoWindow")]
+    NoWindow,
+    #[error("Bounds")]
+    NoBounds,
+}
+
+impl<TCaptureFormat: ScreenCaptureFormat> ScreenCaptureSource<TCaptureFormat> {
+    #[allow(clippy::too_many_arguments)]
+    pub async fn init(
+        target: &ScreenCaptureTarget,
+        show_cursor: bool,
+        max_fps: u32,
+        video_tx: Sender<(TCaptureFormat::VideoFormat, f64)>,
+        audio_tx: Option<Sender<(ffmpeg::frame::Audio, f64)>>,
+        start_time: SystemTime,
+        tokio_handle: tokio::runtime::Handle,
+    ) -> Result<Self, ScreenCaptureInitError> {
+        cap_fail::fail!("ScreenCaptureSource::init");
+
+        let display = target.display().ok_or(ScreenCaptureInitError::NoDisplay)?;
+
+        let fps = max_fps.min(display.refresh_rate() as u32);
+
+        let crop_bounds = match target {
+            ScreenCaptureTarget::Display { .. } => None,
+            ScreenCaptureTarget::Window { id } => {
+                let window = Window::from_id(&id).ok_or(ScreenCaptureInitError::NoWindow)?;
+
+                #[cfg(target_os = "macos")]
+                {
+                    let raw_display_bounds = display
+                        .raw_handle()
+                        .logical_bounds()
+                        .ok_or(ScreenCaptureInitError::NoBounds)?;
+                    let raw_window_bounds = window
+                        .raw_handle()
+                        .logical_bounds()
+                        .ok_or(ScreenCaptureInitError::NoBounds)?;
+
+                    Some(LogicalBounds::new(
+                        LogicalPosition::new(
+                            raw_window_bounds.position().x() - raw_display_bounds.position().x(),
+                            raw_window_bounds.position().y() - raw_display_bounds.position().y(),
+                        ),
+                        raw_window_bounds.size(),
+                    ))
+                }
+
+                #[cfg(windows)]
+                {
+                    let raw_display_position = display
+                        .raw_handle()
+                        .physical_position()
+                        .ok_or(ScreenCaptureInitError::NoBounds)?;
+                    let raw_window_bounds = window
+                        .raw_handle()
+                        .physical_bounds()
+                        .ok_or(ScreenCaptureInitError::NoBounds)?;
+
+                    Some(PhysicalBounds::new(
+                        PhysicalPosition::new(
+                            raw_window_bounds.position().x() - raw_display_position.x(),
+                            raw_window_bounds.position().y() - raw_display_position.y(),
+                        ),
+                        raw_window_bounds.size(),
+                    ))
+                }
+            }
+            ScreenCaptureTarget::Area {
+                bounds: relative_bounds,
+                ..
+            } => {
+                #[cfg(target_os = "macos")]
+                {
+                    Some(*relative_bounds)
+                }
+
+                #[cfg(windows)]
+                {
+                    let raw_display_size = display
+                        .physical_size()
+                        .ok_or(ScreenCaptureInitError::NoBounds)?;
+                    let logical_display_size = display
+                        .logical_size()
+                        .ok_or(ScreenCaptureInitError::NoBounds)?;
+
+                    Some(PhysicalBounds::new(
+                        PhysicalPosition::new(
+                            (relative_bounds.position().x() / logical_display_size.width())
+                                * raw_display_size.width(),
+                            (relative_bounds.position().y() / logical_display_size.height())
+                                * raw_display_size.height(),
+                        ),
+                        PhysicalSize::new(
+                            (relative_bounds.size().width() / logical_display_size.width())
+                                * raw_display_size.width(),
+                            (relative_bounds.size().height() / logical_display_size.height())
+                                * raw_display_size.height(),
+                        ),
+                    ))
+                }
+            }
+        };
+
+        let output_size = crop_bounds
+            .and_then(|b| {
+                #[cfg(target_os = "macos")]
+                {
+                    let logical_size = b.size();
+                    let scale = display.raw_handle().scale()?;
+                    Some(PhysicalSize::new(
+                        logical_size.width() * scale,
+                        logical_size.height() * scale,
+                    ))
+                }
+
+                #[cfg(windows)]
+                Some(b.size().map(|v| (v / 2.0).floor() * 2.0))
+            })
+            .or_else(|| display.physical_size())
+            .ok_or(ScreenCaptureInitError::NoBounds)?;
+
+        Ok(Self {
+            config: Config {
+                display: display.id(),
+                crop_bounds,
+                fps,
+                show_cursor,
+            },
+            video_info: VideoInfo::from_raw_ffmpeg(
+                TCaptureFormat::pixel_format(),
+                output_size.width() as u32,
+                output_size.height() as u32,
+                fps,
+            ),
+            video_tx,
+            audio_tx,
+            tokio_handle,
+            start_time,
+            _phantom: std::marker::PhantomData,
+        })
+    }
+
+    pub fn info(&self) -> VideoInfo {
+        self.video_info
+    }
+
+    pub fn audio_info(&self) -> AudioInfo {
+        TCaptureFormat::audio_info()
+    }
+}
+
+pub fn list_displays() -> Vec<(CaptureDisplay, Display)> {
+    cap_displays::Display::list()
+        .into_iter()
+        .filter_map(|display| {
+            Some((
+                CaptureDisplay {
+                    id: display.id(),
+                    name: display.name()?,
+                    refresh_rate: display.raw_handle().refresh_rate() as u32,
+                },
+                display,
+            ))
+        })
+        .collect()
+}
+
+pub fn list_windows() -> Vec<(CaptureWindow, Window)> {
+    cap_displays::Window::list()
+        .into_iter()
+        .flat_map(|v| {
+            let name = v.name()?;
+
+            if name.is_empty() {
+                return None;
+            }
+
+            #[cfg(target_os = "macos")]
+            {
+                if v.raw_handle().level() != Some(0)
+                    || v.owner_name().filter(|v| v == "Window Server").is_some()
+                {
+                    return None;
+                }
+            }
+
+            #[cfg(windows)]
+            {
+                if !v.raw_handle().is_valid() || !v.raw_handle().is_on_screen() {
+                    return None;
+                }
+            }
+
+            Some((
+                CaptureWindow {
+                    id: v.id(),
+                    name,
+                    owner_name: v.owner_name()?,
+                    bounds: v.display_relative_logical_bounds()?,
+                    refresh_rate: v.display()?.raw_handle().refresh_rate() as u32,
+                },
+                v,
+            ))
+        })
+        .collect()
+}
diff --git a/crates/recording/src/sources/screen_capture/windows.rs b/crates/recording/src/sources/screen_capture/windows.rs
new file mode 100644
index 0000000000..e8edaae4e2
--- /dev/null
+++ b/crates/recording/src/sources/screen_capture/windows.rs
@@ -0,0 +1,534 @@
+use super::*;
+use ::windows::{Graphics::Capture::GraphicsCaptureItem, Win32::Graphics::Direct3D11::D3D11_BOX};
+use cap_fail::fail_err;
+use cpal::traits::{DeviceTrait, HostTrait};
+use kameo::prelude::*;
+use scap_ffmpeg::*;
+use std::{
+    collections::VecDeque,
+    time::{Duration, Instant},
+};
+use tracing::{info, trace};
+
+const WINDOW_DURATION: Duration = Duration::from_secs(3);
+const LOG_INTERVAL: Duration = Duration::from_secs(5);
+const MAX_DROP_RATE_THRESHOLD: f64 = 0.25;
+
+#[derive(Debug)]
+pub struct AVFrameCapture;
+
+impl AVFrameCapture {
+    const PIXEL_FORMAT: scap_direct3d::PixelFormat = scap_direct3d::PixelFormat::R8G8B8A8Unorm;
+}
+
+impl ScreenCaptureFormat for AVFrameCapture {
+    type VideoFormat = ffmpeg::frame::Video;
+
+    fn pixel_format() -> ffmpeg::format::Pixel {
+        scap_direct3d::PixelFormat::R8G8B8A8Unorm.as_ffmpeg()
+    }
+
+    fn audio_info() -> AudioInfo {
+        let host = cpal::default_host();
+        let output_device = host.default_output_device().unwrap();
+        let supported_config = output_device.default_output_config().unwrap();
+
+        let mut info = AudioInfo::from_stream_config(&supported_config);
+
+        info.sample_format = ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Packed);
+
+        info
+    }
+}
+
+struct FrameHandler {
+    capturer: WeakActorRef<ScreenCaptureActor>,
+    start_time: SystemTime,
+    frames_dropped: u32,
+    last_cleanup: Instant,
+    last_log: Instant,
+    frame_events: VecDeque<(Instant, bool)>,
+    video_tx: Sender<(ffmpeg::frame::Video, f64)>,
+}
+
+impl Actor for FrameHandler {
+    type Args = Self;
+    type Error = ();
+
+    async fn on_start(args: Self::Args, self_actor: ActorRef<Self>) -> Result<Self, Self::Error> {
+        if let Some(capturer) = args.capturer.upgrade() {
+            self_actor.link(&capturer).await;
+        }
+
+        Ok(args)
+    }
+
+    async fn on_link_died(
+        &mut self,
+        actor_ref: WeakActorRef<Self>,
+        id: ActorID,
+        _: ActorStopReason,
+    ) -> Result<std::ops::ControlFlow<ActorStopReason>, Self::Error> {
+        if self.capturer.id() == id
+            && let Some(self_actor) = actor_ref.upgrade()
+        {
+            let _ = self_actor.stop_gracefully().await;
+
+            return Ok(std::ops::ControlFlow::Break(ActorStopReason::Normal));
+        }
+
+        Ok(std::ops::ControlFlow::Continue(()))
+    }
+}
+
+impl FrameHandler {
+    // Helper function to clean up old frame events
+    fn cleanup_old_events(&mut self, now: Instant) {
+        let cutoff = now - WINDOW_DURATION;
+        while let Some(&(timestamp, _)) = self.frame_events.front() {
+            if timestamp < cutoff {
+                self.frame_events.pop_front();
+            } else {
+                break;
+            }
+        }
+    }
+
+    // Helper function to calculate current drop rate
+    fn calculate_drop_rate(&mut self) -> (f64, usize, usize) {
+        let now = Instant::now();
+        self.cleanup_old_events(now);
+
+        if self.frame_events.is_empty() {
+            return (0.0, 0, 0);
+        }
+
+        let total_frames = self.frame_events.len();
+        let dropped_frames = self
+            .frame_events
+            .iter()
+            .filter(|(_, dropped)| *dropped)
+            .count();
+        let drop_rate = dropped_frames as f64 / total_frames as f64;
+
+        (drop_rate, dropped_frames, total_frames)
+    }
+}
+
+impl Message<NewFrame> for FrameHandler {
+    type Reply = ();
+
+    async fn handle(
+        &mut self,
+        mut msg: NewFrame,
+        ctx: &mut kameo::prelude::Context<Self, Self::Reply>,
+    ) -> Self::Reply {
+        let Ok(elapsed) = msg.display_time.duration_since(self.start_time) else {
+            return;
+        };
+
+        msg.ff_frame.set_pts(Some(
+            (elapsed.as_secs_f64() * AV_TIME_BASE_Q.den as f64) as i64,
+        ));
+
+        let now = Instant::now();
+        let frame_dropped = match self
+            .video_tx
+            .try_send((msg.ff_frame, elapsed.as_secs_f64()))
+        {
+            Err(flume::TrySendError::Disconnected(_)) => {
+                warn!("Pipeline disconnected");
+                let _ = ctx.actor_ref().stop_gracefully().await;
+                return;
+            }
+            Err(flume::TrySendError::Full(_)) => {
+                warn!("Screen capture sender is full, dropping frame");
+                self.frames_dropped += 1;
+                true
+            }
+            _ => false,
+        };
+
+        self.frame_events.push_back((now, frame_dropped));
+
+        if now.duration_since(self.last_cleanup) > Duration::from_millis(100) {
+            self.cleanup_old_events(now);
+            self.last_cleanup = now;
+        }
+
+        // Check drop rate and potentially exit
+        let (drop_rate, dropped_count, total_count) = self.calculate_drop_rate();
+
+        if drop_rate > MAX_DROP_RATE_THRESHOLD && total_count >= 10 {
+            error!(
+                "High frame drop rate detected: {:.1}% ({}/{} frames in last {}s). Exiting capture.",
+                drop_rate * 100.0,
+                dropped_count,
+                total_count,
+                WINDOW_DURATION.as_secs()
+            );
+            let _ = ctx.actor_ref().stop_gracefully().await;
+            return;
+            // return ControlFlow::Break(Err("Recording can't keep up with screen capture. Try reducing your display's resolution or refresh rate.".to_string()));
+        }
+
+        // Periodic logging of drop rate
+        if now.duration_since(self.last_log) > LOG_INTERVAL && total_count > 0 {
+            info!(
+                "Frame drop rate: {:.1}% ({}/{} frames, total dropped: {})",
+                drop_rate * 100.0,
+                dropped_count,
+                total_count,
+                self.frames_dropped
+            );
+            self.last_log = now;
+        }
+    }
+}
+
+#[derive(Clone, Debug, thiserror::Error)]
+enum SourceError {
+    #[error("NoDisplay: Id '{0}'")]
+    NoDisplay(DisplayId),
+    #[error("AsCaptureItem: {0}")]
+    AsCaptureItem(::windows::core::Error),
+    #[error("StartCapturingVideo/{0}")]
+    StartCapturingVideo(SendError<StartCapturing, StartCapturingError>),
+    #[error("CreateAudioCapture/{0}")]
+    CreateAudioCapture(scap_cpal::CapturerError),
+    #[error("StartCapturingAudio/{0}")]
+    StartCapturingAudio(SendError<audio::StartCapturing, cpal::PlayStreamError>),
+    #[error("Closed")]
+    Closed,
+}
+
+impl PipelineSourceTask for ScreenCaptureSource<AVFrameCapture> {
+    // #[instrument(skip_all)]
+    fn run(
+        &mut self,
+        ready_signal: crate::pipeline::task::PipelineReadySignal,
+        control_signal: crate::pipeline::control::PipelineControlSignal,
+    ) -> Result<(), String> {
+        let video_tx = self.video_tx.clone();
+        let audio_tx = self.audio_tx.clone();
+
+        let start_time = self.start_time;
+
+        // Frame drop rate tracking state
+        let config = self.config.clone();
+
+        self.tokio_handle
+            .block_on(async move {
+                let (error_tx, error_rx) = flume::bounded(1);
+                let capturer = ScreenCaptureActor::spawn(ScreenCaptureActor::new(error_tx));
+
+                let frame_handler = FrameHandler::spawn(FrameHandler {
+                    capturer: capturer.downgrade(),
+                    video_tx,
+                    start_time,
+                    frame_events: Default::default(),
+                    frames_dropped: Default::default(),
+                    last_cleanup: Instant::now(),
+                    last_log: Instant::now(),
+                });
+
+                let mut settings = scap_direct3d::Settings {
+                    pixel_format: AVFrameCapture::PIXEL_FORMAT,
+                    crop: config.crop_bounds.map(|b| {
+                        let position = b.position();
+                        let size = b.size().map(|v| (v / 2.0).floor() * 2.0);
+
+                        D3D11_BOX {
+                            left: position.x() as u32,
+                            top: position.y() as u32,
+                            right: (position.x() + size.width()) as u32,
+                            bottom: (position.y() + size.height()) as u32,
+                            front: 0,
+                            back: 1,
+                        }
+                    }),
+                    ..Default::default()
+                };
+
+                if let Ok(true) = scap_direct3d::Settings::can_is_border_required() {
+                    settings.is_border_required = Some(false);
+                }
+
+                if let Ok(true) = scap_direct3d::Settings::can_is_cursor_capture_enabled() {
+                    settings.is_cursor_capture_enabled = Some(config.show_cursor);
+                }
+
+                if let Ok(true) = scap_direct3d::Settings::can_min_update_interval() {
+                    settings.min_update_interval =
+                        Some(Duration::from_secs_f64(1.0 / config.fps as f64));
+                }
+
+                let display = Display::from_id(&config.display)
+                    .ok_or_else(|| SourceError::NoDisplay(config.display))?;
+
+                let capture_item = display
+                    .raw_handle()
+                    .try_as_capture_item()
+                    .map_err(SourceError::AsCaptureItem)?;
+
+                capturer
+                    .ask(StartCapturing {
+                        target: capture_item,
+                        settings,
+                        frame_handler: frame_handler.clone().recipient(),
+                    })
+                    .await
+                    .map_err(SourceError::StartCapturingVideo)?;
+
+                let audio_capture = if let Some(audio_tx) = audio_tx {
+                    let audio_capture = WindowsAudioCapture::spawn(
+                        WindowsAudioCapture::new(audio_tx, start_time)
+                            .map_err(SourceError::CreateAudioCapture)?,
+                    );
+
+                    audio_capture
+                        .ask(audio::StartCapturing)
+                        .await
+                        .map_err(SourceError::StartCapturingAudio)?;
+
+                    Some(audio_capture)
+                } else {
+                    None
+                };
+
+                let _ = ready_signal.send(Ok(()));
+
+                let stop = async move {
+                    let _ = capturer.ask(StopCapturing).await;
+                    let _ = capturer.stop_gracefully().await;
+
+                    if let Some(audio_capture) = audio_capture {
+                        let _ = audio_capture.ask(StopCapturing).await;
+                        let _ = audio_capture.stop_gracefully().await;
+                    }
+                };
+
+                loop {
+                    use futures::future::Either;
+
+                    match futures::future::select(
+                        error_rx.recv_async(),
+                        control_signal.receiver().recv_async(),
+                    )
+                    .await
+                    {
+                        Either::Left((Ok(_), _)) => {
+                            error!("Screen capture closed");
+                            stop.await;
+                            return Err(SourceError::Closed);
+                        }
+                        Either::Right((Ok(ctrl), _)) => {
+                            if let Control::Shutdown = ctrl {
+                                stop.await;
+                                return Ok(());
+                            }
+                        }
+                        _ => {
+                            warn!("Screen capture recv channels shutdown, exiting.");
+
+                            stop.await;
+
+                            return Ok(());
+                        }
+                    }
+                }
+            })
+            .map_err(|e| e.to_string())
+    }
+}
+
+#[derive(Actor)]
+struct ScreenCaptureActor {
+    capture_handle: Option<scap_direct3d::Capturer>,
+    error_tx: Sender<()>,
+}
+
+impl ScreenCaptureActor {
+    pub fn new(error_tx: Sender<()>) -> Self {
+        Self {
+            capture_handle: None,
+            error_tx,
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct StartCapturing {
+    pub target: GraphicsCaptureItem,
+    pub settings: scap_direct3d::Settings,
+    pub frame_handler: Recipient<NewFrame>,
+    // error_handler: Option<Recipient<CaptureError>>,
+}
+
+#[derive(Debug, Clone, thiserror::Error)]
+pub enum StartCapturingError {
+    #[error("AlreadyCapturing")]
+    AlreadyCapturing,
+    #[error("CreateCapturer/{0}")]
+    CreateCapturer(scap_direct3d::NewCapturerError),
+    #[error("StartCapturer/{0}")]
+    StartCapturer(scap_direct3d::StartCapturerError),
+}
+
+pub struct NewFrame {
+    pub ff_frame: ffmpeg::frame::Video,
+    pub display_time: SystemTime,
+}
+
+impl Message<StartCapturing> for ScreenCaptureActor {
+    type Reply = Result<(), StartCapturingError>;
+
+    async fn handle(
+        &mut self,
+        msg: StartCapturing,
+        _: &mut Context<Self, Self::Reply>,
+    ) -> Self::Reply {
+        if self.capture_handle.is_some() {
+            return Err(StartCapturingError::AlreadyCapturing);
+        }
+
+        fail_err!(
+            "WindowsScreenCapture.StartCapturing",
+            StartCapturingError::CreateCapturer(scap_direct3d::NewCapturerError::NotSupported)
+        );
+
+        trace!("Starting capturer with settings: {:?}", &msg.settings);
+
+        let mut capture_handle = scap_direct3d::Capturer::new(msg.target, msg.settings)
+            .map_err(StartCapturingError::CreateCapturer)?;
+
+        let error_tx = self.error_tx.clone();
+        capture_handle
+            .start(
+                move |frame| {
+                    let display_time = SystemTime::now();
+                    let ff_frame = frame.as_ffmpeg().unwrap();
+
+                    let _ = msg
+                        .frame_handler
+                        .tell(NewFrame {
+                            ff_frame,
+                            display_time,
+                        })
+                        .try_send();
+
+                    Ok(())
+                },
+                move || {
+                    let _ = error_tx.send(());
+
+                    Ok(())
+                },
+            )
+            .map_err(StartCapturingError::StartCapturer)?;
+
+        info!("Capturer started");
+
+        self.capture_handle = Some(capture_handle);
+
+        Ok(())
+    }
+}
+
+impl Message<StopCapturing> for ScreenCaptureActor {
+    type Reply = Result<(), StopCapturingError>;
+
+    async fn handle(
+        &mut self,
+        _: StopCapturing,
+        _: &mut Context<Self, Self::Reply>,
+    ) -> Self::Reply {
+        let Some(mut capturer) = self.capture_handle.take() else {
+            return Err(StopCapturingError::NotCapturing);
+        };
+
+        if let Err(e) = capturer.stop() {
+            error!("Silently failed to stop Windows capturer: {}", e);
+        }
+
+        info!("stopped windows capturer");
+
+        Ok(())
+    }
+}
+
+use audio::WindowsAudioCapture;
+pub mod audio {
+    use super::*;
+    use cpal::{PauseStreamError, PlayStreamError};
+
+    #[derive(Actor)]
+    pub struct WindowsAudioCapture {
+        capturer: scap_cpal::Capturer,
+    }
+
+    impl WindowsAudioCapture {
+        pub fn new(
+            audio_tx: Sender<(ffmpeg::frame::Audio, f64)>,
+            start_time: SystemTime,
+        ) -> Result<Self, scap_cpal::CapturerError> {
+            let mut i = 0;
+            let capturer = scap_cpal::create_capturer(
+                move |data, _: &cpal::InputCallbackInfo, config| {
+                    use scap_ffmpeg::*;
+
+                    let timestamp = SystemTime::now();
+                    let mut ff_frame = data.as_ffmpeg(config);
+
+                    let Ok(elapsed) = timestamp.duration_since(start_time) else {
+                        warn!("Skipping audio frame {i} as elapsed time is invalid");
+                        return;
+                    };
+
+                    ff_frame.set_pts(Some(
+                        (elapsed.as_secs_f64() * AV_TIME_BASE_Q.den as f64) as i64,
+                    ));
+
+                    let _ = audio_tx.send((ff_frame, elapsed.as_secs_f64()));
+                    i += 1;
+                },
+                move |e| {
+                    dbg!(e);
+                },
+            )?;
+
+            Ok(Self { capturer })
+        }
+    }
+
+    #[derive(Clone)]
+    pub struct StartCapturing;
+
+    impl Message<StartCapturing> for WindowsAudioCapture {
+        type Reply = Result<(), PlayStreamError>;
+
+        async fn handle(
+            &mut self,
+            _: StartCapturing,
+            _: &mut Context<Self, Self::Reply>,
+        ) -> Self::Reply {
+            self.capturer.play()?;
+
+            Ok(())
+        }
+    }
+
+    impl Message<StopCapturing> for WindowsAudioCapture {
+        type Reply = Result<(), PauseStreamError>;
+
+        async fn handle(
+            &mut self,
+            _: StopCapturing,
+            _: &mut Context<Self, Self::Reply>,
+        ) -> Self::Reply {
+            self.capturer.pause()?;
+
+            Ok(())
+        }
+    }
+}
diff --git a/crates/recording/src/studio_recording.rs b/crates/recording/src/studio_recording.rs
index 149d8bba05..8011c0a158 100644
--- a/crates/recording/src/studio_recording.rs
+++ b/crates/recording/src/studio_recording.rs
@@ -1,30 +1,26 @@
-use std::{
-    collections::HashMap,
-    path::{Path, PathBuf},
-    sync::Arc,
-    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
-};
-
-use cap_media::{
-    MediaError,
-    feeds::{AudioInputFeed, CameraFeed},
-    pipeline::{Pipeline, RealTimeClock},
-    platform::Bounds,
-    sources::{AudioInputSource, CameraSource, ScreenCaptureFormat, ScreenCaptureTarget},
-};
+use cap_media::MediaError;
 use cap_media_encoders::{H264Encoder, MP4File, OggFile, OpusEncoder};
 use cap_media_info::VideoInfo;
 use cap_project::{CursorEvents, StudioRecordingMeta};
 use cap_utils::spawn_actor;
 use flume::Receiver;
 use relative_path::RelativePathBuf;
+use std::{
+    collections::HashMap,
+    path::{Path, PathBuf},
+    sync::Arc,
+    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
+};
 use tokio::sync::{Mutex, oneshot};
 use tracing::{debug, info, trace};
 
 use crate::{
     ActorError, RecordingBaseInputs, RecordingError,
     capture_pipeline::{MakeCapturePipeline, ScreenCaptureMethod, create_screen_capture},
-    cursor::{CursorActor, Cursors, spawn_cursor_recorder},
+    cursor::{CursorActor, Cursors /*spawn_cursor_recorder*/, spawn_cursor_recorder},
+    feeds::{AudioInputFeed, CameraFeed},
+    pipeline::Pipeline,
+    sources::{AudioInputSource, CameraSource, ScreenCaptureFormat, ScreenCaptureTarget},
 };
 
 #[allow(clippy::large_enum_variant)]
@@ -45,7 +41,7 @@ enum StudioRecordingActorState {
 
 pub enum StudioRecordingActorControlMessage {
     Pause(oneshot::Sender<Result<(), RecordingError>>),
-    Resume(oneshot::Sender<Result<(), RecordingError>>),
+    Resume(oneshot::Sender<Result<(), CreateSegmentPipelineError>>),
     Stop(oneshot::Sender<Result<CompletedStudioRecording, RecordingError>>),
     Cancel(oneshot::Sender<Result<(), RecordingError>>),
 }
@@ -72,12 +68,11 @@ pub struct PipelineOutput {
 
 pub struct ScreenPipelineOutput {
     pub inner: PipelineOutput,
-    pub bounds: Bounds,
     pub video_info: VideoInfo,
 }
 
 struct StudioRecordingPipeline {
-    pub inner: Pipeline<RealTimeClock<()>>,
+    pub inner: Pipeline,
     pub screen: ScreenPipelineOutput,
     pub microphone: Option<PipelineOutput>,
     pub camera: Option<CameraPipelineInfo>,
@@ -94,7 +89,6 @@ struct CursorPipeline {
 pub struct StudioRecordingHandle {
     ctrl_tx: flume::Sender<StudioRecordingActorControlMessage>,
     pub capture_target: ScreenCaptureTarget,
-    pub bounds: Bounds,
 }
 
 macro_rules! send_message {
@@ -117,7 +111,7 @@ impl StudioRecordingHandle {
         send_message!(self.ctrl_tx, StudioRecordingActorControlMessage::Pause)
     }
 
-    pub async fn resume(&self) -> Result<(), RecordingError> {
+    pub async fn resume(&self) -> Result<(), CreateSegmentPipelineError> {
         send_message!(self.ctrl_tx, StudioRecordingActorControlMessage::Resume)
     }
 
@@ -126,13 +120,22 @@ impl StudioRecordingHandle {
     }
 }
 
+#[derive(Debug, thiserror::Error)]
+pub enum SpawnStudioRecordingError {
+    #[error("{0}")]
+    Media(#[from] MediaError),
+    #[error("{0}")]
+    PipelineCreationError(#[from] CreateSegmentPipelineError),
+}
+
 pub async fn spawn_studio_recording_actor<'a>(
     id: String,
     recording_dir: PathBuf,
     base_inputs: RecordingBaseInputs<'a>,
     camera_feed: Option<Arc<Mutex<CameraFeed>>>,
     custom_cursor_capture: bool,
-) -> Result<(StudioRecordingHandle, oneshot::Receiver<Result<(), String>>), RecordingError> {
+) -> Result<(StudioRecordingHandle, oneshot::Receiver<Result<(), String>>), SpawnStudioRecordingError>
+{
     ensure_dir(&recording_dir)?;
 
     let (done_tx, done_rx) = oneshot::channel();
@@ -148,11 +151,9 @@ pub async fn spawn_studio_recording_actor<'a>(
     let start_time = SystemTime::now();
     let start_instant = Instant::now();
 
-    // debug!("screen capture: {screen_source:#?}");
-
     if let Some(camera_feed) = &camera_feed {
         let camera_feed = camera_feed.lock().await;
-        // debug!("camera device info: {:#?}", camera_feed.camera_info());
+        debug!("camera device info: {:#?}", camera_feed.camera_info());
         debug!("camera video info: {:#?}", camera_feed.video_info());
     }
 
@@ -164,7 +165,7 @@ pub async fn spawn_studio_recording_actor<'a>(
     let mut segment_pipeline_factory = SegmentPipelineFactory::new(
         segments_dir,
         cursors_dir,
-        base_inputs.capture_target,
+        base_inputs.capture_target.clone(),
         audio_input_feed,
         base_inputs.capture_system_audio,
         camera_feed,
@@ -184,10 +185,6 @@ pub async fn spawn_studio_recording_actor<'a>(
 
     trace!("spawning recording actor");
 
-    let bounds = pipeline.screen.bounds;
-
-    debug!("screen bounds: {bounds:?}");
-
     let base_inputs = base_inputs.clone();
     let fps = pipeline.screen.video_info.fps();
 
@@ -219,7 +216,7 @@ pub async fn spawn_studio_recording_actor<'a>(
             }
         };
 
-        info!("recording actor finished");
+        info!("recording actor finished: {:?}", &result);
 
         let _ = done_tx.send(result.map_err(|v| v.to_string()));
     });
@@ -228,7 +225,6 @@ pub async fn spawn_studio_recording_actor<'a>(
         StudioRecordingHandle {
             ctrl_tx,
             capture_target: base_inputs.capture_target,
-            bounds,
         },
         done_rx,
     ))
@@ -266,8 +262,12 @@ async fn run_actor_iteration(
         actor: &mut StudioRecordingActor,
         segment_start_time: f64,
     ) -> Result<(Cursors, u32), RecordingError> {
+        tracing::info!("pipeline shuting down");
+
         pipeline.inner.shutdown().await?;
 
+        tracing::info!("pipeline shutdown");
+
         let segment_stop_time = current_time_f64();
 
         let cursors = if let Some(cursor) = &mut pipeline.cursor {
@@ -319,18 +319,18 @@ async fn run_actor_iteration(
         } => {
             tokio::select! {
                 result = &mut pipeline_done_rx => {
-                    return match result {
-                        Ok(Ok(())) => {
-                            if let Some(cursor) = &mut pipeline.cursor
-                                && let Some(actor) = cursor.actor.take() {
-                                    actor.stop().await;
-                                }
-
-                            Ok(None)
-                        },
+                    let res = match result {
+                        Ok(Ok(())) => Ok(None),
                         Ok(Err(e)) => Err(StudioRecordingActorError::Other(e)),
                         Err(_) => Err(StudioRecordingActorError::PipelineReceiverDropped),
+                    };
+
+                    if let Some(cursor) = &mut pipeline.cursor
+                        && let Some(actor) = cursor.actor.take() {
+                        actor.stop().await;
                     }
+
+                    return res;
                 },
                 msg = ctrl_rx.recv_async() => {
                     match msg {
@@ -344,7 +344,14 @@ async fn run_actor_iteration(
                                 segment_start_instant,
                             },
                         ),
-                        Err(_) => return Err(StudioRecordingActorError::ControlReceiverDropped),
+                        Err(_) => {
+                            if let Some(cursor) = &mut pipeline.cursor
+                                && let Some(actor) = cursor.actor.take() {
+                                actor.stop().await;
+                            }
+
+                            return Err(StudioRecordingActorError::ControlReceiverDropped)
+                        },
                     }
                 }
             }
@@ -410,6 +417,8 @@ async fn run_actor_iteration(
                 State::Paused { cursors, .. } => stop_recording(actor, cursors).await,
             };
 
+            println!("recording successfully stopped");
+
             send_response!(tx, result);
             None
         }
@@ -606,13 +615,13 @@ impl SegmentPipelineFactory {
             StudioRecordingPipeline,
             oneshot::Receiver<Result<(), String>>,
         ),
-        RecordingError,
+        CreateSegmentPipelineError,
     > {
         let result = create_segment_pipeline(
             &self.segments_dir,
             &self.cursors_dir,
             self.index,
-            self.capture_target,
+            self.capture_target.clone(),
             &self.audio_input_feed,
             self.capture_system_audio,
             self.camera_feed.as_deref(),
@@ -630,6 +639,24 @@ impl SegmentPipelineFactory {
     }
 }
 
+#[derive(Debug, thiserror::Error)]
+pub enum CreateSegmentPipelineError {
+    #[error("NoDisplay")]
+    NoDisplay,
+    #[error("NoBounds")]
+    NoBounds,
+    #[error("PipelineBuild/{0}")]
+    PipelineBuild(MediaError),
+    #[error("PipelinePlay/{0}")]
+    PipelinePlay(MediaError),
+    #[error("Actor/{0}")]
+    Actor(#[from] ActorError),
+    #[error("{0}")]
+    Recording(#[from] RecordingError),
+    #[error("{0}")]
+    Media(#[from] MediaError),
+}
+
 #[tracing::instrument(skip_all, name = "segment", fields(index = index))]
 #[allow(clippy::too_many_arguments)]
 async fn create_segment_pipeline(
@@ -650,7 +677,7 @@ async fn create_segment_pipeline(
         StudioRecordingPipeline,
         oneshot::Receiver<Result<(), String>>,
     ),
-    RecordingError,
+    CreateSegmentPipelineError,
 > {
     let system_audio = if capture_system_audio {
         let (tx, rx) = flume::bounded(64);
@@ -659,17 +686,21 @@ async fn create_segment_pipeline(
         (None, None)
     };
 
+    let display = capture_target
+        .display()
+        .ok_or(CreateSegmentPipelineError::NoDisplay)?;
+    let crop_bounds = capture_target
+        .cursor_crop()
+        .ok_or(CreateSegmentPipelineError::NoBounds)?;
+
     let (screen_source, screen_rx) = create_screen_capture(
         &capture_target,
-        false,
         !custom_cursor_capture,
         120,
         system_audio.0,
         start_time,
     )
     .await?;
-    #[cfg(target_os = "macos")]
-    let screen_crop_ratio = screen_source.crop_ratio();
 
     let camera_feed = match camera_feed.as_ref() {
         Some(camera_feed) => Some(camera_feed.lock().await),
@@ -679,15 +710,13 @@ async fn create_segment_pipeline(
 
     let dir = ensure_dir(&segments_dir.join(format!("segment-{index}")))?;
 
-    let clock = RealTimeClock::<()>::new();
-    let mut pipeline_builder = Pipeline::builder(clock);
+    let mut pipeline_builder = Pipeline::builder();
 
     let screen_output_path = dir.join("display.mp4");
 
     trace!("preparing segment pipeline {index}");
 
     let screen = {
-        let bounds = *screen_source.get_bounds();
         let video_info = screen_source.info();
 
         let (pipeline_builder_, screen_timestamp_rx) =
@@ -711,7 +740,6 @@ async fn create_segment_pipeline(
                 path: screen_output_path,
                 first_timestamp_rx: screen_timestamp_rx,
             },
-            bounds,
             video_info,
         }
     };
@@ -860,25 +888,20 @@ async fn create_segment_pipeline(
         None
     };
 
+    let (mut pipeline, pipeline_done_rx) = pipeline_builder
+        .build()
+        .await
+        .map_err(CreateSegmentPipelineError::PipelineBuild)?;
+
+    pipeline
+        .play()
+        .await
+        .map_err(CreateSegmentPipelineError::PipelinePlay)?;
+
     let cursor = custom_cursor_capture.then(move || {
         let cursor = spawn_cursor_recorder(
-            screen.bounds,
-            #[cfg(target_os = "macos")]
-            cap_displays::Display::list()
-                .into_iter()
-                .find(|m| match &capture_target {
-                    ScreenCaptureTarget::Screen { id }
-                    | ScreenCaptureTarget::Area { screen: id, .. } => {
-                        m.raw_handle().inner().id == *id
-                    }
-                    ScreenCaptureTarget::Window { id } => {
-                        m.raw_handle().inner().id
-                            == cap_media::platform::display_for_window(*id).unwrap().id
-                    }
-                })
-                .unwrap(),
-            #[cfg(target_os = "macos")]
-            screen_crop_ratio,
+            crop_bounds,
+            display,
             cursors_dir.to_path_buf(),
             prev_cursors,
             next_cursors_id,
@@ -891,10 +914,6 @@ async fn create_segment_pipeline(
         }
     });
 
-    let (mut pipeline, pipeline_done_rx) = pipeline_builder.build().await?;
-
-    pipeline.play().await?;
-
     info!("pipeline playing");
 
     Ok((
diff --git a/crates/rendering/Cargo.toml b/crates/rendering/Cargo.toml
index b1fd004d3d..51555ec574 100644
--- a/crates/rendering/Cargo.toml
+++ b/crates/rendering/Cargo.toml
@@ -16,7 +16,6 @@ cap-cursor-info = { path = "../cursor-info" }
 ffmpeg-hw-device = { path = "../ffmpeg-hw-device" }
 tokio.workspace = true
 ffmpeg.workspace = true
-ffmpeg-sys-next.workspace = true
 futures = { workspace = true }
 futures-intrusive = "0.5.0"
 image = "0.25.2"
diff --git a/crates/rendering/src/decoder/ffmpeg.rs b/crates/rendering/src/decoder/ffmpeg.rs
index 5a72a9f5cd..05f53ddd2d 100644
--- a/crates/rendering/src/decoder/ffmpeg.rs
+++ b/crates/rendering/src/decoder/ffmpeg.rs
@@ -1,3 +1,5 @@
+use ffmpeg::{format, frame, software, sys::AVHWDeviceType};
+use log::debug;
 use std::{
     cell::RefCell,
     collections::BTreeMap,
@@ -5,10 +7,6 @@ use std::{
     rc::Rc,
     sync::{Arc, mpsc},
 };
-
-use ffmpeg::{format, frame, software};
-use ffmpeg_sys_next::AVHWDeviceType;
-use log::debug;
 use tokio::sync::oneshot;
 
 use super::{FRAME_CACHE_SIZE, VideoDecoderMessage, pts_to_frame};
diff --git a/crates/scap-cpal/Cargo.toml b/crates/scap-cpal/Cargo.toml
new file mode 100644
index 0000000000..dae7575133
--- /dev/null
+++ b/crates/scap-cpal/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "scap-cpal"
+version = "0.1.0"
+edition = "2024"
+license = "MIT"
+
+[dependencies]
+cpal.workspace = true
+thiserror.workspace = true
+
+[lints]
+workspace = true
diff --git a/crates/scap-cpal/src/lib.rs b/crates/scap-cpal/src/lib.rs
new file mode 100644
index 0000000000..73a89c3add
--- /dev/null
+++ b/crates/scap-cpal/src/lib.rs
@@ -0,0 +1,70 @@
+use cpal::{
+    BuildStreamError, DefaultStreamConfigError, InputCallbackInfo, PauseStreamError,
+    PlayStreamError, Stream, StreamConfig, StreamError, traits::StreamTrait,
+};
+use thiserror::Error;
+
+#[derive(Clone, Error, Debug)]
+pub enum CapturerError {
+    #[error("NoDevice")]
+    NoDevice,
+    #[error("DefaultConfig: {0}")]
+    DefaultConfig(#[from] DefaultStreamConfigError),
+    #[error("BuildStream: {0}")]
+    BuildStream(#[from] BuildStreamError),
+}
+
+pub fn create_capturer(
+    mut data_callback: impl FnMut(&cpal::Data, &InputCallbackInfo, &StreamConfig) + Send + 'static,
+    error_callback: impl FnMut(StreamError) + Send + 'static,
+) -> Result<Capturer, CapturerError> {
+    use cpal::traits::{DeviceTrait, HostTrait};
+
+    let host = cpal::default_host();
+    let output_device = host
+        .default_output_device()
+        .ok_or(CapturerError::NoDevice)?;
+    let supported_config = output_device.default_output_config()?;
+    let config = supported_config.clone().into();
+
+    let stream = output_device.build_input_stream_raw(
+        &config,
+        supported_config.sample_format(),
+        {
+            let config = config.clone();
+            move |data, info: &InputCallbackInfo| data_callback(data, info, &config)
+        },
+        error_callback,
+        None,
+    )?;
+
+    Ok(Capturer {
+        stream,
+        config,
+        _output_device: output_device,
+        _host: host,
+        _supported_config: supported_config,
+    })
+}
+
+pub struct Capturer {
+    stream: Stream,
+    config: StreamConfig,
+    _output_device: cpal::Device,
+    _host: cpal::Host,
+    _supported_config: cpal::SupportedStreamConfig,
+}
+
+impl Capturer {
+    pub fn play(&self) -> Result<(), PlayStreamError> {
+        self.stream.play()
+    }
+
+    pub fn pause(&self) -> Result<(), PauseStreamError> {
+        self.stream.pause()
+    }
+
+    pub fn config(&self) -> &StreamConfig {
+        &self.config
+    }
+}
diff --git a/crates/scap-direct3d/Cargo.toml b/crates/scap-direct3d/Cargo.toml
index c043816f24..1684ecab6f 100644
--- a/crates/scap-direct3d/Cargo.toml
+++ b/crates/scap-direct3d/Cargo.toml
@@ -2,12 +2,14 @@
 name = "scap-direct3d"
 version = "0.1.0"
 edition = "2024"
+license = "MIT"
 
 [target.'cfg(windows)'.dependencies]
 windows = { workspace = true, features = [
 	"System",
 	"Graphics_Capture",
 	"Graphics_DirectX_Direct3D11",
+	"Foundation_Metadata",
 	"Win32_Graphics_Gdi",
 	"Win32_Graphics_Direct3D",
 	"Win32_Graphics_Direct3D11",
@@ -19,5 +21,12 @@ windows = { workspace = true, features = [
 	"Win32_UI_WindowsAndMessaging",
 ] }
 
+[dev-dependencies]
+cap-displays = { path = "../displays" }
+scap-ffmpeg = { path = "../scap-ffmpeg" }
+
 [lints]
 workspace = true
+
+[dependencies]
+thiserror.workspace = true
diff --git a/crates/scap-direct3d/examples/cli.rs b/crates/scap-direct3d/examples/cli.rs
index d7ad710afe..1e6c9f5879 100644
--- a/crates/scap-direct3d/examples/cli.rs
+++ b/crates/scap-direct3d/examples/cli.rs
@@ -1,27 +1,54 @@
-use scap_direct3d::{Capturer, Display, PixelFormat, Settings};
-use std::time::Duration;
-
 fn main() {
-    let display = Display::primary().unwrap();
+    #[cfg(windows)]
+    windows::main();
+}
+
+#[cfg(windows)]
+mod windows {
+    use cap_displays::*;
+    use scap_direct3d::{Capturer, PixelFormat, Settings};
+    use scap_ffmpeg::*;
+    use std::time::Duration;
+    use windows::Win32::Graphics::Direct3D11::D3D11_BOX;
+
+    pub fn main() {
+        let display = Display::primary();
+        let display = display.raw_handle();
+
+        let capturer = Capturer::new(
+            display.try_as_capture_item().unwrap(),
+            Settings {
+                is_border_required: Some(false),
+                is_cursor_capture_enabled: Some(true),
+                pixel_format: PixelFormat::R8G8B8A8Unorm,
+                crop: Some(D3D11_BOX {
+                    left: 0,
+                    top: 0,
+                    right: 500,
+                    bottom: 400,
+                    front: 0,
+                    back: 1,
+                }),
+                ..Default::default()
+            },
+        );
 
-    let capturer = Capturer::new(
-        display.try_as_capture_item().unwrap(),
-        Settings {
-            is_border_required: Some(true),
-            is_cursor_capture_enabled: Some(true),
-            pixel_format: PixelFormat::R8G8B8A8Unorm,
-        },
-    );
+        let capture_handle = capturer.start(
+            |frame| {
+                dbg!(&frame);
 
-    let capture_handle = capturer.start(|frame| {
-        dbg!(frame);
+                let ff_frame = frame.as_ffmpeg()?;
+                dbg!(ff_frame.width(), ff_frame.height(), ff_frame.format());
 
-        Ok(())
-    });
+                Ok(())
+            },
+            || Ok(()),
+        );
 
-    std::thread::sleep(Duration::from_secs(3));
+        std::thread::sleep(Duration::from_secs(3));
 
-    capture_handle.stop().unwrap();
+        capture_handle.stop().unwrap();
 
-    std::thread::sleep(Duration::from_secs(3));
+        std::thread::sleep(Duration::from_secs(3));
+    }
 }
diff --git a/crates/scap-direct3d/src/lib.rs b/crates/scap-direct3d/src/lib.rs
index 5638fbda36..cca7ce570c 100644
--- a/crates/scap-direct3d/src/lib.rs
+++ b/crates/scap-direct3d/src/lib.rs
@@ -7,49 +7,53 @@ use std::{
     sync::{
         Arc,
         atomic::{AtomicBool, Ordering},
+        mpsc::RecvError,
     },
+    thread::JoinHandle,
+    time::Duration,
 };
 
 use windows::{
-    Foundation::TypedEventHandler,
+    Foundation::{Metadata::ApiInformation, TypedEventHandler},
     Graphics::{
-        Capture::{Direct3D11CaptureFrame, Direct3D11CaptureFramePool, GraphicsCaptureItem},
+        Capture::{
+            Direct3D11CaptureFrame, Direct3D11CaptureFramePool, GraphicsCaptureItem,
+            GraphicsCaptureSession,
+        },
         DirectX::{Direct3D11::IDirect3DDevice, DirectXPixelFormat},
     },
     Win32::{
-        Foundation::{HANDLE, HMODULE, LPARAM, POINT, S_FALSE, WPARAM},
+        Foundation::{HANDLE, HMODULE, LPARAM, S_FALSE, WPARAM},
         Graphics::{
             Direct3D::D3D_DRIVER_TYPE_HARDWARE,
             Direct3D11::{
+                D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE, D3D11_BOX,
                 D3D11_CPU_ACCESS_READ, D3D11_CPU_ACCESS_WRITE, D3D11_MAP_READ_WRITE,
                 D3D11_MAPPED_SUBRESOURCE, D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC,
-                D3D11_USAGE_STAGING, D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext,
-                ID3D11Texture2D,
+                D3D11_USAGE_DEFAULT, D3D11_USAGE_STAGING, D3D11CreateDevice, ID3D11Device,
+                ID3D11DeviceContext, ID3D11Texture2D,
             },
             Dxgi::{
-                Common::{DXGI_FORMAT, DXGI_SAMPLE_DESC},
+                Common::{DXGI_FORMAT, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_SAMPLE_DESC},
                 IDXGIDevice,
             },
-            Gdi::{HMONITOR, MONITOR_DEFAULTTONULL, MonitorFromPoint},
         },
         System::{
             Threading::GetThreadId,
             WinRT::{
                 CreateDispatcherQueueController, DQTAT_COM_NONE, DQTYPE_THREAD_CURRENT,
                 Direct3D11::{CreateDirect3D11DeviceFromDXGIDevice, IDirect3DDxgiInterfaceAccess},
-                DispatcherQueueOptions,
-                Graphics::Capture::IGraphicsCaptureItemInterop,
-                RO_INIT_MULTITHREADED, RoInitialize,
+                DispatcherQueueOptions, RO_INIT_MULTITHREADED, RoInitialize,
             },
         },
         UI::WindowsAndMessaging::{
             DispatchMessageW, GetMessageW, MSG, PostThreadMessageW, TranslateMessage, WM_QUIT,
         },
     },
-    core::{IInspectable, Interface},
+    core::{HSTRING, IInspectable, Interface},
 };
 
-#[derive(Default, Clone, Copy)]
+#[derive(Default, Clone, Copy, Debug)]
 #[repr(i32)]
 pub enum PixelFormat {
     #[default]
@@ -59,98 +63,209 @@ pub enum PixelFormat {
 impl PixelFormat {
     pub fn as_directx(&self) -> DirectXPixelFormat {
         match self {
-            PixelFormat::R8G8B8A8Unorm => DirectXPixelFormat::R8G8B8A8UIntNormalized,
+            Self::R8G8B8A8Unorm => DirectXPixelFormat::R8G8B8A8UIntNormalized,
+        }
+    }
+
+    pub fn as_dxgi(&self) -> DXGI_FORMAT {
+        match self {
+            Self::R8G8B8A8Unorm => DXGI_FORMAT_R8G8B8A8_UNORM,
         }
     }
 }
 
-#[derive(Clone)]
-pub struct CaptureItem {
-    inner: GraphicsCaptureItem,
+pub fn is_supported() -> windows::core::Result<bool> {
+    Ok(ApiInformation::IsApiContractPresentByMajor(
+        &HSTRING::from("Windows.Foundation.UniversalApiContract"),
+        8,
+    )? && GraphicsCaptureSession::IsSupported()?)
 }
 
-pub struct Display {
-    inner: HMONITOR,
+#[derive(Clone, Default, Debug)]
+pub struct Settings {
+    pub is_border_required: Option<bool>,
+    pub is_cursor_capture_enabled: Option<bool>,
+    pub min_update_interval: Option<Duration>,
+    pub pixel_format: PixelFormat,
+    pub crop: Option<D3D11_BOX>,
 }
 
-impl Display {
-    pub fn primary() -> Option<Self> {
-        let monitor = unsafe { MonitorFromPoint(POINT { x: 0, y: 0 }, MONITOR_DEFAULTTONULL) };
-        if monitor.is_invalid() {
-            return None;
-        }
-        Some(Self { inner: monitor })
+impl Settings {
+    pub fn can_is_border_required() -> windows::core::Result<bool> {
+        ApiInformation::IsPropertyPresent(
+            &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"),
+            &HSTRING::from("IsBorderRequired"),
+        )
     }
 
-    pub fn try_as_capture_item(&self) -> windows::core::Result<CaptureItem> {
-        let interop = windows::core::factory::<GraphicsCaptureItem, IGraphicsCaptureItemInterop>()?;
-        let inner = unsafe { interop.CreateForMonitor(self.inner) }?;
+    pub fn can_is_cursor_capture_enabled() -> windows::core::Result<bool> {
+        ApiInformation::IsPropertyPresent(
+            &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"),
+            &HSTRING::from("IsCursorCaptureEnabled"),
+        )
+    }
 
-        Ok(CaptureItem { inner })
+    pub fn can_min_update_interval() -> windows::core::Result<bool> {
+        ApiInformation::IsPropertyPresent(
+            &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"),
+            &HSTRING::from("MinUpdateInterval"),
+        )
     }
 }
 
-#[derive(Default)]
-pub struct Settings {
-    pub is_border_required: Option<bool>,
-    pub is_cursor_capture_enabled: Option<bool>,
-    pub pixel_format: PixelFormat,
+#[derive(Clone, Debug, thiserror::Error)]
+pub enum NewCapturerError {
+    #[error("NotSupported")]
+    NotSupported,
+    #[error("BorderNotSupported")]
+    BorderNotSupported,
+    #[error("CursorNotSupported")]
+    CursorNotSupported,
+    #[error("UpdateIntervalNotSupported")]
+    UpdateIntervalNotSupported,
+    #[error("CreateRunner/{0}")]
+    CreateRunner(#[from] StartRunnerError),
+    #[error("RecvTimeout")]
+    RecvTimeout(#[from] RecvError),
+    #[error("Other: {0}")]
+    Other(#[from] windows::core::Error),
 }
 
 pub struct Capturer {
-    item: CaptureItem,
+    stop_flag: Arc<AtomicBool>,
+    item: GraphicsCaptureItem,
     settings: Settings,
+    thread_handle: Option<JoinHandle<()>>,
 }
 
 impl Capturer {
-    pub fn new(item: CaptureItem, settings: Settings) -> Self {
-        Self { item, settings }
+    pub fn new(
+        item: GraphicsCaptureItem,
+        settings: Settings,
+    ) -> Result<Capturer, NewCapturerError> {
+        if !is_supported()? {
+            return Err(NewCapturerError::NotSupported);
+        }
+
+        if settings.is_border_required.is_some() && !Settings::can_is_border_required()? {
+            return Err(NewCapturerError::BorderNotSupported);
+        }
+
+        if settings.is_cursor_capture_enabled.is_some()
+            && !Settings::can_is_cursor_capture_enabled()?
+        {
+            return Err(NewCapturerError::CursorNotSupported);
+        }
+
+        if settings.min_update_interval.is_some() && !Settings::can_min_update_interval()? {
+            return Err(NewCapturerError::UpdateIntervalNotSupported);
+        }
+
+        let stop_flag = Arc::new(AtomicBool::new(false));
+
+        Ok(Capturer {
+            stop_flag,
+            item,
+            settings,
+            thread_handle: None,
+        })
     }
+}
 
+#[derive(Clone, Debug, thiserror::Error)]
+pub enum StartCapturerError {
+    #[error("AlreadyStarted")]
+    AlreadyStarted,
+    #[error("StartFailed/{0}")]
+    StartFailed(StartRunnerError),
+    #[error("RecvFailed")]
+    RecvFailed(RecvError),
+}
+impl Capturer {
     pub fn start(
-        self,
+        &mut self,
         callback: impl FnMut(Frame) -> windows::core::Result<()> + Send + 'static,
-    ) -> CaptureHandle {
-        let stop_flag = Arc::new(AtomicBool::new(false));
+        closed_callback: impl FnMut() -> windows::core::Result<()> + Send + 'static,
+    ) -> Result<(), StartCapturerError> {
+        if self.thread_handle.is_some() {
+            return Err(StartCapturerError::AlreadyStarted);
+        }
+
+        let (started_tx, started_rx) = std::sync::mpsc::channel();
+
+        let item = self.item.clone();
+        let settings = self.settings.clone();
+        let stop_flag = self.stop_flag.clone();
+
         let thread_handle = std::thread::spawn({
-            let stop_flag = stop_flag.clone();
             move || {
-                let _ = dbg!(run(self.item, self.settings, callback, stop_flag));
+                if let Err(e) = unsafe { RoInitialize(RO_INIT_MULTITHREADED) }
+                    && e.code() != S_FALSE
+                {
+                    return;
+                    // return Err(CreateRunnerError::FailedToInitializeWinRT);
+                }
+
+                match Runner::start(item, settings, callback, closed_callback, stop_flag) {
+                    Ok(runner) => {
+                        let _ = started_tx.send(Ok(()));
+
+                        runner.run();
+                    }
+                    Err(e) => {
+                        let _ = started_tx.send(Err(e));
+                    }
+                };
             }
         });
 
-        CaptureHandle {
-            stop_flag,
-            thread_handle,
-        }
+        started_rx
+            .recv()
+            .map_err(StartCapturerError::RecvFailed)?
+            .map_err(StartCapturerError::StartFailed)?;
+
+        self.thread_handle = Some(thread_handle);
+
+        Ok(())
     }
 }
 
-pub struct CaptureHandle {
-    stop_flag: Arc<AtomicBool>,
-    thread_handle: std::thread::JoinHandle<()>,
+#[derive(Clone, Debug, thiserror::Error)]
+pub enum StopCapturerError {
+    #[error("NotStarted")]
+    NotStarted,
+    #[error("PostMessageFailed")]
+    PostMessageFailed,
+    #[error("ThreadJoinFailed")]
+    ThreadJoinFailed,
 }
 
-impl CaptureHandle {
-    pub fn stop(self) -> Result<(), &'static str> {
+impl Capturer {
+    pub fn stop(&mut self) -> Result<(), StopCapturerError> {
+        let Some(thread_handle) = self.thread_handle.take() else {
+            return Err(StopCapturerError::NotStarted);
+        };
+
         self.stop_flag.store(true, Ordering::Relaxed);
 
-        let handle = HANDLE(self.thread_handle.as_raw_handle());
+        let handle = HANDLE(thread_handle.as_raw_handle());
         let thread_id = unsafe { GetThreadId(handle) };
 
         while let Err(e) =
             unsafe { PostThreadMessageW(thread_id, WM_QUIT, WPARAM::default(), LPARAM::default()) }
         {
-            if self.thread_handle.is_finished() {
+            if thread_handle.is_finished() {
                 break;
             }
 
             if e.code().0 != -2147023452 {
-                return Err("Failed to post message");
+                return Err(StopCapturerError::PostMessageFailed);
             }
         }
 
-        self.thread_handle.join().map_err(|_| "Join failed")
+        thread_handle
+            .join()
+            .map_err(|_| StopCapturerError::ThreadJoinFailed)
     }
 }
 
@@ -219,6 +334,7 @@ impl<'a> Frame<'a> {
 
         let texture = texture.unwrap();
 
+        // Copies GPU only texture to CPU-mappable texture
         unsafe {
             self.d3d_context.CopyResource(&texture, &self.texture);
         };
@@ -287,121 +403,216 @@ impl<'a> FrameBuffer<'a> {
     }
 }
 
-fn run(
-    item: CaptureItem,
-    settings: Settings,
-    mut callback: impl FnMut(Frame) -> windows::core::Result<()> + Send + 'static,
-    stop_flag: Arc<AtomicBool>,
-) -> Result<(), &'static str> {
-    if let Err(e) = unsafe { RoInitialize(RO_INIT_MULTITHREADED) }
-        && e.code() != S_FALSE
-    {
-        return Err("Failed to initialise WinRT");
-    }
+#[derive(Clone, Debug, thiserror::Error)]
+pub enum StartRunnerError {
+    #[error("Failed to initialize WinRT")]
+    FailedToInitializeWinRT,
+    #[error("DispatchQueue: {0}")]
+    DispatchQueue(windows::core::Error),
+    #[error("D3DDevice: {0}")]
+    D3DDevice(windows::core::Error),
+    #[error("Direct3DDevice: {0}")]
+    Direct3DDevice(windows::core::Error),
+    #[error("FramePool: {0}")]
+    FramePool(windows::core::Error),
+    #[error("CaptureSession: {0}")]
+    CaptureSession(windows::core::Error),
+    #[error("CropTexture: {0}")]
+    CropTexture(windows::core::Error),
+    #[error("RegisterFrameArrived: {0}")]
+    RegisterFrameArrived(windows::core::Error),
+    #[error("RegisterClosed: {0}")]
+    RegisterClosed(windows::core::Error),
+    #[error("StartCapture: {0}")]
+    StartCapture(windows::core::Error),
+    #[error("Other: {0}")]
+    Other(#[from] windows::core::Error),
+}
+
+#[derive(Clone)]
+struct Runner {
+    _session: GraphicsCaptureSession,
+    _frame_pool: Direct3D11CaptureFramePool,
+}
+
+impl Runner {
+    fn start(
+        item: GraphicsCaptureItem,
+        settings: Settings,
+        mut callback: impl FnMut(Frame) -> windows::core::Result<()> + Send + 'static,
+        mut closed_callback: impl FnMut() -> windows::core::Result<()> + Send + 'static,
+        stop_flag: Arc<AtomicBool>,
+    ) -> Result<Self, StartRunnerError> {
+        let queue_options = DispatcherQueueOptions {
+            dwSize: std::mem::size_of::<DispatcherQueueOptions>() as u32,
+            threadType: DQTYPE_THREAD_CURRENT,
+            apartmentType: DQTAT_COM_NONE,
+        };
+
+        let _controller = unsafe { CreateDispatcherQueueController(queue_options) }
+            .map_err(StartRunnerError::DispatchQueue)?;
+
+        let mut d3d_device = None;
+        let mut d3d_context = None;
 
-    let queue_options = DispatcherQueueOptions {
-        dwSize: std::mem::size_of::<DispatcherQueueOptions>() as u32,
-        threadType: DQTYPE_THREAD_CURRENT,
-        apartmentType: DQTAT_COM_NONE,
-    };
-
-    let _controller = unsafe { CreateDispatcherQueueController(queue_options) }
-        .map_err(|_| "Failed to create dispatcher queue controller")?;
-
-    let mut d3d_device = None;
-    let mut d3d_context = None;
-
-    unsafe {
-        D3D11CreateDevice(
-            None,
-            D3D_DRIVER_TYPE_HARDWARE,
-            HMODULE::default(),
-            Default::default(),
-            None,
-            D3D11_SDK_VERSION,
-            Some(&mut d3d_device),
-            None,
-            Some(&mut d3d_context),
+        unsafe {
+            D3D11CreateDevice(
+                None,
+                D3D_DRIVER_TYPE_HARDWARE,
+                HMODULE::default(),
+                Default::default(),
+                None,
+                D3D11_SDK_VERSION,
+                Some(&mut d3d_device),
+                None,
+                Some(&mut d3d_context),
+            )
+        }
+        .map_err(StartRunnerError::D3DDevice)?;
+
+        let d3d_device = d3d_device.unwrap();
+        let d3d_context = d3d_context.unwrap();
+
+        let direct3d_device = (|| {
+            let dxgi_device = d3d_device.cast::<IDXGIDevice>()?;
+            let inspectable = unsafe { CreateDirect3D11DeviceFromDXGIDevice(&dxgi_device) }?;
+            inspectable.cast::<IDirect3DDevice>()
+        })()
+        .map_err(StartRunnerError::Direct3DDevice)?;
+
+        let frame_pool = Direct3D11CaptureFramePool::Create(
+            &direct3d_device,
+            PixelFormat::R8G8B8A8Unorm.as_directx(),
+            1,
+            item.Size()?,
         )
-    }
-    .map_err(|_| "Failed to create d3d11 device")?;
-
-    let d3d_device = d3d_device.unwrap();
-    let d3d_context = d3d_context.unwrap();
-
-    let direct3d_device = (|| {
-        let dxgi_device = d3d_device.cast::<IDXGIDevice>()?;
-        let inspectable = unsafe { CreateDirect3D11DeviceFromDXGIDevice(&dxgi_device) }?;
-        inspectable.cast::<IDirect3DDevice>()
-    })()
-    .map_err(|_| "Failed to create direct3d device")?;
-
-    let frame_pool = Direct3D11CaptureFramePool::Create(
-        &direct3d_device,
-        PixelFormat::R8G8B8A8Unorm.as_directx(),
-        1,
-        item.inner.Size().map_err(|_| "Item size")?,
-    )
-    .map_err(|_| "Failed to create frame pool")?;
-
-    let session = frame_pool
-        .CreateCaptureSession(&item.inner)
-        .map_err(|_| "Failed to create capture session")?;
-
-    if let Some(border_required) = settings.is_border_required {
-        session
-            .SetIsBorderRequired(border_required)
-            .map_err(|_| "Failed to set border required")?;
-    }
+        .map_err(StartRunnerError::FramePool)?;
 
-    if let Some(cursor_capture_enabled) = settings.is_cursor_capture_enabled {
-        session
-            .SetIsCursorCaptureEnabled(cursor_capture_enabled)
-            .map_err(|_| "Failed to set cursor capture enabled")?;
-    }
+        let session = frame_pool
+            .CreateCaptureSession(&item)
+            .map_err(StartRunnerError::CaptureSession)?;
 
-    let _frame_arrived_token = frame_pool
-        .FrameArrived(
-            &TypedEventHandler::<Direct3D11CaptureFramePool, IInspectable>::new(
-                move |frame_pool, _| {
-                    if stop_flag.load(Ordering::Relaxed) {
-                        return Ok(());
-                    }
+        if let Some(border_required) = settings.is_border_required {
+            session.SetIsBorderRequired(border_required)?;
+        }
 
-                    let frame = frame_pool
-                        .as_ref()
-                        .expect("FrameArrived parameter was None")
-                        .TryGetNextFrame()?;
-
-                    let size = frame.ContentSize()?;
-
-                    let surface = frame.Surface()?;
-                    let dxgi_interface = surface.cast::<IDirect3DDxgiInterfaceAccess>()?;
-                    let texture = unsafe { dxgi_interface.GetInterface::<ID3D11Texture2D>() }?;
-
-                    (callback)(Frame {
-                        width: size.Width as u32,
-                        height: size.Height as u32,
-                        pixel_format: settings.pixel_format,
-                        inner: frame,
-                        texture,
-                        d3d_context: &d3d_context,
-                        d3d_device: &d3d_device,
-                    })
-                },
-            ),
+        if let Some(cursor_capture_enabled) = settings.is_cursor_capture_enabled {
+            session.SetIsCursorCaptureEnabled(cursor_capture_enabled)?;
+        }
+
+        if let Some(min_update_interval) = settings.min_update_interval {
+            session.SetMinUpdateInterval(min_update_interval.into())?;
+        }
+
+        let crop_data = settings
+            .crop
+            .map(|crop| {
+                let desc = D3D11_TEXTURE2D_DESC {
+                    Width: (crop.right - crop.left),
+                    Height: (crop.bottom - crop.top),
+                    MipLevels: 1,
+                    ArraySize: 1,
+                    Format: settings.pixel_format.as_dxgi(),
+                    SampleDesc: DXGI_SAMPLE_DESC {
+                        Count: 1,
+                        Quality: 0,
+                    },
+                    Usage: D3D11_USAGE_DEFAULT,
+                    BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
+                    CPUAccessFlags: 0,
+                    MiscFlags: 0,
+                };
+
+                let mut texture = None;
+                unsafe { d3d_device.CreateTexture2D(&desc, None, Some(&mut texture)) }
+                    .map_err(StartRunnerError::CropTexture)?;
+
+                Ok::<_, StartRunnerError>((texture.unwrap(), crop))
+            })
+            .transpose()?;
+
+        frame_pool
+            .FrameArrived(
+                &TypedEventHandler::<Direct3D11CaptureFramePool, IInspectable>::new(
+                    move |frame_pool, _| {
+                        if stop_flag.load(Ordering::Relaxed) {
+                            return Ok(());
+                        }
+
+                        let frame = frame_pool
+                            .as_ref()
+                            .expect("FrameArrived parameter was None")
+                            .TryGetNextFrame()?;
+
+                        let size = frame.ContentSize()?;
+
+                        let surface = frame.Surface()?;
+                        let dxgi_interface = surface.cast::<IDirect3DDxgiInterfaceAccess>()?;
+                        let texture = unsafe { dxgi_interface.GetInterface::<ID3D11Texture2D>() }?;
+
+                        let frame = if let Some((cropped_texture, crop)) = crop_data.clone() {
+                            unsafe {
+                                d3d_context.CopySubresourceRegion(
+                                    &cropped_texture,
+                                    0,
+                                    0,
+                                    0,
+                                    0,
+                                    &texture,
+                                    0,
+                                    Some(&crop),
+                                );
+                            }
+
+                            Frame {
+                                width: crop.right - crop.left,
+                                height: crop.bottom - crop.top,
+                                pixel_format: settings.pixel_format,
+                                inner: frame,
+                                texture: cropped_texture,
+                                d3d_context: &d3d_context,
+                                d3d_device: &d3d_device,
+                            }
+                        } else {
+                            Frame {
+                                width: size.Width as u32,
+                                height: size.Height as u32,
+                                pixel_format: settings.pixel_format,
+                                inner: frame,
+                                texture,
+                                d3d_context: &d3d_context,
+                                d3d_device: &d3d_device,
+                            }
+                        };
+
+                        (callback)(frame)
+                    },
+                ),
+            )
+            .map_err(StartRunnerError::RegisterFrameArrived)?;
+
+        item.Closed(
+            &TypedEventHandler::<GraphicsCaptureItem, IInspectable>::new(move |_, _| {
+                closed_callback()
+            }),
         )
-        .map_err(|_| "Failed to register frame arrived handler")?;
+        .map_err(StartRunnerError::RegisterClosed)?;
 
-    session
-        .StartCapture()
-        .map_err(|_| "Failed to start capture")?;
+        session
+            .StartCapture()
+            .map_err(StartRunnerError::StartCapture)?;
 
-    let mut message = MSG::default();
-    while unsafe { GetMessageW(&mut message, None, 0, 0) }.as_bool() {
-        let _ = unsafe { TranslateMessage(&message) };
-        unsafe { DispatchMessageW(&message) };
+        Ok(Self {
+            _session: session,
+            _frame_pool: frame_pool,
+        })
     }
 
-    Ok(())
+    fn run(self) {
+        let mut message = MSG::default();
+        while unsafe { GetMessageW(&mut message, None, 0, 0) }.as_bool() {
+            let _ = unsafe { TranslateMessage(&message) };
+            unsafe { DispatchMessageW(&message) };
+        }
+    }
 }
diff --git a/crates/scap-ffmpeg/Cargo.toml b/crates/scap-ffmpeg/Cargo.toml
index 6b43ba926e..a318010cde 100644
--- a/crates/scap-ffmpeg/Cargo.toml
+++ b/crates/scap-ffmpeg/Cargo.toml
@@ -2,10 +2,14 @@
 name = "scap-ffmpeg"
 version = "0.1.0"
 edition = "2024"
+license = "MIT"
 
 [dependencies]
 ffmpeg = { workspace = true }
 
+scap-cpal = { optional = true, path = "../scap-cpal" }
+cpal = { workspace = true }
+
 [target.'cfg(windows)'.dependencies]
 scap-direct3d = { path = "../scap-direct3d" }
 windows = { workspace = true, features = [] }
@@ -16,6 +20,8 @@ cidre = { workspace = true }
 
 [dev-dependencies]
 futures = { workspace = true }
+tokio = { workspace = true }
+cap-displays = { path = "../displays" }
 
 [lints]
 workspace = true
diff --git a/crates/scap-ffmpeg/examples/cli.rs b/crates/scap-ffmpeg/examples/cli.rs
index 46d6654fb9..878abf3421 100644
--- a/crates/scap-ffmpeg/examples/cli.rs
+++ b/crates/scap-ffmpeg/examples/cli.rs
@@ -1,7 +1,9 @@
+use cap_displays::*;
 use scap_ffmpeg::*;
 use std::time::Duration;
 
-pub fn main() {
+#[tokio::main]
+pub async fn main() {
     #[cfg(windows)]
     {
         use scap_direct3d::*;
@@ -17,15 +19,17 @@ pub fn main() {
             },
         );
 
-        let capture_handle = capturer.start(|frame| {
-            use scap_ffmpeg::AsFFmpeg;
+        let capture_handle = capturer
+            .start(|frame| {
+                use scap_ffmpeg::AsFFmpeg;
 
-            let ff_frame = frame.as_ffmpeg()?;
+                let ff_frame = frame.as_ffmpeg()?;
 
-            dbg!(ff_frame.width(), ff_frame.height(), ff_frame.format());
+                dbg!(ff_frame.width(), ff_frame.height(), ff_frame.format());
 
-            Ok(())
-        });
+                Ok(())
+            })
+            .unwrap();
 
         std::thread::sleep(Duration::from_secs(3));
 
@@ -39,29 +43,32 @@ pub fn main() {
         use futures::executor::block_on;
         use scap_screencapturekit::*;
 
-        let display = block_on(Display::primary()).expect("Primary display not found");
+        let display = Display::primary();
 
         let config = StreamCfgBuilder::default()
             .with_fps(60.0)
-            .with_width(display.width())
-            .with_height(display.height())
+            .with_width(display.physical_size().width() as usize)
+            .with_height(display.physical_size().height() as usize)
             .build();
 
-        let capturer = Capturer::builder(display.as_content_filter(), config)
-            .with_output_sample_buf_cb(|frame| {
-                let Frame::Screen(video_frame) = frame else {
-                    return;
-                };
+        let capturer = Capturer::builder(
+            display.raw_handle().as_content_filter().await.unwrap(),
+            config,
+        )
+        .with_output_sample_buf_cb(|frame| {
+            let Frame::Screen(video_frame) = frame else {
+                return;
+            };
 
-                let ff_frame = video_frame.as_ffmpeg().unwrap();
+            let ff_frame = video_frame.as_ffmpeg().unwrap();
 
-                dbg!(ff_frame.width(), ff_frame.height(), ff_frame.format());
-            })
-            .with_stop_with_err_cb(|stream, error| {
-                dbg!(stream, error);
-            })
-            .build()
-            .expect("Failed to build capturer");
+            dbg!(ff_frame.width(), ff_frame.height(), ff_frame.format());
+        })
+        .with_stop_with_err_cb(|stream, error| {
+            dbg!(stream, error);
+        })
+        .build()
+        .expect("Failed to build capturer");
 
         block_on(capturer.start()).expect("Failed to start capturing");
 
diff --git a/crates/scap-ffmpeg/src/cpal.rs b/crates/scap-ffmpeg/src/cpal.rs
new file mode 100644
index 0000000000..c3afb79ff6
--- /dev/null
+++ b/crates/scap-ffmpeg/src/cpal.rs
@@ -0,0 +1,81 @@
+use cpal::{SampleFormat, StreamConfig};
+use ffmpeg::format::{Sample, sample};
+
+pub trait DataExt {
+    fn as_ffmpeg(&self, config: &StreamConfig) -> ffmpeg::frame::Audio;
+}
+
+impl DataExt for ::cpal::Data {
+    fn as_ffmpeg(&self, config: &StreamConfig) -> ffmpeg::frame::Audio {
+        let format_typ = sample::Type::Packed;
+
+        let sample_size = self.sample_format().sample_size();
+        let sample_count = self.bytes().len() / (sample_size * config.channels as usize);
+
+        let mut ffmpeg_frame = ffmpeg::frame::Audio::new(
+            match self.sample_format() {
+                SampleFormat::U8 => Sample::U8(format_typ),
+                SampleFormat::I16 => Sample::I16(format_typ),
+                SampleFormat::I32 => Sample::I32(format_typ),
+                SampleFormat::F32 => Sample::F32(format_typ),
+                SampleFormat::F64 => Sample::F64(format_typ),
+                _ => panic!("Unsupported sample format"),
+            },
+            sample_count,
+            ffmpeg::ChannelLayout::default(config.channels as i32),
+        );
+
+        if matches!(format_typ, sample::Type::Planar) {
+            for i in 0..config.channels {
+                let plane_size = sample_count * sample_size as usize;
+                let base = (i as usize) * plane_size;
+
+                ffmpeg_frame
+                    .plane_data_mut(i as usize)
+                    .copy_from_slice(&self.bytes()[base..base + plane_size]);
+            }
+        } else {
+            ffmpeg_frame.data_mut(0).copy_from_slice(self.bytes());
+        }
+
+        ffmpeg_frame.set_rate(config.sample_rate.0);
+
+        ffmpeg_frame
+    }
+}
+
+pub trait PlanarData {
+    fn plane_data(&self, index: usize) -> &[u8];
+
+    fn plane_data_mut(&mut self, index: usize) -> &mut [u8];
+}
+
+impl PlanarData for ffmpeg::frame::Audio {
+    #[inline]
+    fn plane_data(&self, index: usize) -> &[u8] {
+        if index >= self.planes() {
+            panic!("out of bounds");
+        }
+
+        unsafe {
+            std::slice::from_raw_parts(
+                (*self.as_ptr()).data[index],
+                (*self.as_ptr()).linesize[0] as usize,
+            )
+        }
+    }
+
+    #[inline]
+    fn plane_data_mut(&mut self, index: usize) -> &mut [u8] {
+        if index >= self.planes() {
+            panic!("out of bounds");
+        }
+
+        unsafe {
+            std::slice::from_raw_parts_mut(
+                (*self.as_mut_ptr()).data[index],
+                (*self.as_ptr()).linesize[0] as usize,
+            )
+        }
+    }
+}
diff --git a/crates/scap-ffmpeg/src/direct3d.rs b/crates/scap-ffmpeg/src/direct3d.rs
index dcd11d17b7..9c2985d6b3 100644
--- a/crates/scap-ffmpeg/src/direct3d.rs
+++ b/crates/scap-ffmpeg/src/direct3d.rs
@@ -1,3 +1,4 @@
+use ffmpeg::format::Pixel;
 use scap_direct3d::PixelFormat;
 
 pub type AsFFmpegError = windows::core::Error;
@@ -37,3 +38,15 @@ impl<'a> super::AsFFmpeg for scap_direct3d::Frame<'a> {
         }
     }
 }
+
+pub trait PixelFormatExt {
+    fn as_ffmpeg(&self) -> Pixel;
+}
+
+impl PixelFormatExt for PixelFormat {
+    fn as_ffmpeg(&self) -> Pixel {
+        match self {
+            PixelFormat::R8G8B8A8Unorm => Pixel::RGBA,
+        }
+    }
+}
diff --git a/crates/scap-ffmpeg/src/lib.rs b/crates/scap-ffmpeg/src/lib.rs
index 6a8423a279..93beea3086 100644
--- a/crates/scap-ffmpeg/src/lib.rs
+++ b/crates/scap-ffmpeg/src/lib.rs
@@ -8,6 +8,9 @@ mod direct3d;
 #[cfg(windows)]
 pub use direct3d::*;
 
+mod cpal;
+pub use cpal::*;
+
 pub trait AsFFmpeg {
     fn as_ffmpeg(&self) -> Result<ffmpeg::frame::Video, AsFFmpegError>;
 }
diff --git a/crates/scap-ffmpeg/src/screencapturekit.rs b/crates/scap-ffmpeg/src/screencapturekit.rs
index 76b48b0798..c3095b603c 100644
--- a/crates/scap-ffmpeg/src/screencapturekit.rs
+++ b/crates/scap-ffmpeg/src/screencapturekit.rs
@@ -9,7 +9,7 @@ pub enum AsFFmpegError {
     BaseAddrLock(os::Error),
 }
 
-impl<'a> super::AsFFmpeg for scap_screencapturekit::VideoFrame<'a> {
+impl super::AsFFmpeg for scap_screencapturekit::VideoFrame {
     fn as_ffmpeg(&self) -> Result<ffmpeg::frame::Video, AsFFmpegError> {
         let mut image_buf = self.image_buf().retained();
 
diff --git a/crates/scap-screencapturekit/Cargo.toml b/crates/scap-screencapturekit/Cargo.toml
index 87c5ff1199..32d74b4611 100644
--- a/crates/scap-screencapturekit/Cargo.toml
+++ b/crates/scap-screencapturekit/Cargo.toml
@@ -2,7 +2,7 @@
 name = "scap-screencapturekit"
 version = "0.1.0"
 edition = "2024"
-
+license = "MIT"
 
 [dependencies]
 futures = { workspace = true }
@@ -20,3 +20,4 @@ workspace = true
 [dev-dependencies]
 clap = { version = "4.5.40", features = ["derive"] }
 inquire = "0.7.5"
+cap-displays = { path = "../displays" }
diff --git a/crates/scap-screencapturekit/examples/cli.rs b/crates/scap-screencapturekit/examples/cli.rs
index 84d2b390c1..c1bd13416d 100644
--- a/crates/scap-screencapturekit/examples/cli.rs
+++ b/crates/scap-screencapturekit/examples/cli.rs
@@ -1,10 +1,12 @@
+use cap_displays::Display;
 use std::time::Duration;
 
 use futures::executor::block_on;
-use scap_screencapturekit::{Capturer, Display, StreamCfgBuilder, Window};
+use scap_screencapturekit::{Capturer, StreamCfgBuilder};
 
 fn main() {
-    let display = block_on(Display::primary()).expect("Primary display not found");
+    let display = Display::primary();
+    let display = display.raw_handle();
 
     // let windows = block_on(Window::list()).expect("Failed to list windows");
     // let window = windows
@@ -14,22 +16,25 @@ fn main() {
 
     let config = StreamCfgBuilder::default()
         .with_fps(60.0)
-        .with_width(display.width())
-        .with_height(display.height())
+        .with_width(display.physical_size().width() as usize)
+        .with_height(display.physical_size().height() as usize)
         .build();
 
-    let capturer = Capturer::builder(display.as_content_filter(), config)
-        .with_output_sample_buf_cb(|frame| {
-            dbg!(frame.output_type());
-            // if let Some(image_buf) = buf.image_buf() {
-            //     image_buf.show();
-            // }
-        })
-        .with_stop_with_err_cb(|stream, error| {
-            dbg!(stream, error);
-        })
-        .build()
-        .expect("Failed to build capturer");
+    let capturer = Capturer::builder(
+        block_on(display.as_content_filter()).expect("Failed to get display as content filter"),
+        config,
+    )
+    .with_output_sample_buf_cb(|frame| {
+        dbg!(frame.output_type());
+        // if let Some(image_buf) = buf.image_buf() {
+        //     image_buf.show();
+        // }
+    })
+    .with_stop_with_err_cb(|stream, error| {
+        dbg!(stream, error);
+    })
+    .build()
+    .expect("Failed to build capturer");
 
     block_on(capturer.start()).expect("Failed to start capturing");
 
diff --git a/crates/scap-screencapturekit/src/capture.rs b/crates/scap-screencapturekit/src/capture.rs
index 26d6ef058c..18a1a0bb62 100644
--- a/crates/scap-screencapturekit/src/capture.rs
+++ b/crates/scap-screencapturekit/src/capture.rs
@@ -2,12 +2,14 @@ use cidre::{
     arc, cm, cv, define_obj_type, dispatch, ns, objc,
     sc::{self, StreamDelegate, StreamDelegateImpl, StreamOutput, StreamOutputImpl},
 };
-use tracing::warn;
 
 define_obj_type!(
     pub CapturerCallbacks + StreamOutputImpl + StreamDelegateImpl, CapturerCallbacksInner, CAPTURER
 );
 
+unsafe impl Send for CapturerCallbacks {}
+unsafe impl Sync for CapturerCallbacks {}
+
 impl sc::stream::Output for CapturerCallbacks {}
 
 #[objc::add_methods]
@@ -15,26 +17,25 @@ impl sc::stream::OutputImpl for CapturerCallbacks {
     extern "C" fn impl_stream_did_output_sample_buf(
         &mut self,
         _cmd: Option<&objc::Sel>,
-        stream: &sc::Stream,
+        _: &sc::Stream,
         sample_buf: &mut cm::SampleBuf,
         kind: sc::OutputType,
     ) {
         if let Some(cb) = &mut self.inner_mut().did_output_sample_buf_cb {
+            let sample_buf = sample_buf.retained();
             let frame = match kind {
                 sc::OutputType::Screen => {
                     let Some(image_buf) = sample_buf.image_buf().map(|v| v.retained()) else {
-                        warn!("Screen sample buffer has no image buffer");
                         return;
                     };
 
                     Frame::Screen(VideoFrame {
-                        stream,
                         sample_buf,
                         image_buf,
                     })
                 }
-                sc::OutputType::Audio => Frame::Audio(AudioFrame { stream, sample_buf }),
-                sc::OutputType::Mic => Frame::Mic(AudioFrame { stream, sample_buf }),
+                sc::OutputType::Audio => Frame::Audio(AudioFrame { sample_buf }),
+                sc::OutputType::Mic => Frame::Mic(AudioFrame { sample_buf }),
             };
             (cb)(frame);
         }
@@ -67,10 +68,9 @@ pub struct CapturerCallbacksInner {
 }
 
 pub struct Capturer {
-    target: arc::R<sc::ContentFilter>,
-    config: arc::R<sc::StreamCfg>,
     _queue: arc::R<dispatch::Queue>,
     stream: arc::R<sc::Stream>,
+    // READING THIS IS NOT THREAD SAFE, IT JUST HAS TO EXIST
     _callbacks: arc::R<CapturerCallbacks>,
 }
 
@@ -86,14 +86,6 @@ impl Capturer {
         }
     }
 
-    pub fn config(&self) -> &sc::StreamCfg {
-        &self.config
-    }
-
-    pub fn target(&self) -> &sc::ContentFilter {
-        &self.target
-    }
-
     pub async fn start(&self) -> Result<(), arc::R<ns::Error>> {
         self.stream.start().await
     }
@@ -103,67 +95,65 @@ impl Capturer {
     }
 }
 
-pub struct VideoFrame<'a> {
-    stream: &'a sc::Stream,
-    sample_buf: &'a mut cm::SampleBuf,
+pub struct VideoFrame {
+    sample_buf: arc::R<cm::SampleBuf>,
     image_buf: arc::R<cv::ImageBuf>,
 }
 
-impl<'a> VideoFrame<'a> {
-    pub fn stream(&self) -> &sc::Stream {
-        self.stream
-    }
+unsafe impl Send for VideoFrame {}
 
+impl VideoFrame {
     pub fn sample_buf(&self) -> &cm::SampleBuf {
-        self.sample_buf
+        self.sample_buf.as_ref()
     }
 
     pub fn sample_buf_mut(&mut self) -> &mut cm::SampleBuf {
-        self.sample_buf
+        self.sample_buf.as_mut()
     }
 
     pub fn image_buf(&self) -> &cv::ImageBuf {
-        &self.image_buf
+        self.image_buf.as_ref()
     }
 
     pub fn image_buf_mut(&mut self) -> &mut cv::ImageBuf {
-        &mut self.image_buf
+        self.image_buf.as_mut()
     }
 }
 
-pub struct AudioFrame<'a> {
-    stream: &'a sc::Stream,
-    sample_buf: &'a mut cm::SampleBuf,
+pub struct AudioFrame {
+    sample_buf: arc::R<cm::SampleBuf>,
 }
 
-pub enum Frame<'a> {
-    Screen(VideoFrame<'a>),
-    Audio(AudioFrame<'a>),
-    Mic(AudioFrame<'a>),
-}
+impl AudioFrame {
+    pub fn sample_buf(&self) -> &cm::SampleBuf {
+        self.sample_buf.as_ref()
+    }
 
-impl<'a> Frame<'a> {
-    pub fn stream(&self) -> &sc::Stream {
-        match self {
-            Frame::Screen(frame) => frame.stream,
-            Frame::Audio(frame) => frame.stream,
-            Frame::Mic(frame) => frame.stream,
-        }
+    pub fn sample_buf_mut(&mut self) -> &mut cm::SampleBuf {
+        self.sample_buf.as_mut()
     }
+}
+
+pub enum Frame {
+    Screen(VideoFrame),
+    Audio(AudioFrame),
+    Mic(AudioFrame),
+}
 
+impl Frame {
     pub fn sample_buf(&self) -> &cm::SampleBuf {
         match self {
-            Frame::Screen(frame) => frame.sample_buf,
-            Frame::Audio(frame) => frame.sample_buf,
-            Frame::Mic(frame) => frame.sample_buf,
+            Frame::Screen(frame) => frame.sample_buf(),
+            Frame::Audio(frame) => frame.sample_buf(),
+            Frame::Mic(frame) => frame.sample_buf(),
         }
     }
 
     pub fn sample_buf_mut(&mut self) -> &mut cm::SampleBuf {
         match self {
-            Frame::Screen(frame) => frame.sample_buf,
-            Frame::Audio(frame) => frame.sample_buf,
-            Frame::Mic(frame) => frame.sample_buf,
+            Frame::Screen(frame) => frame.sample_buf_mut(),
+            Frame::Audio(frame) => frame.sample_buf_mut(),
+            Frame::Mic(frame) => frame.sample_buf_mut(),
         }
     }
 
@@ -214,8 +204,6 @@ impl CapturerBuilder {
             .map_err(|e| e.retained())?;
 
         Ok(Capturer {
-            target: self.target,
-            config: self.config,
             _queue: queue,
             stream,
             _callbacks: callbacks,
diff --git a/crates/scap-screencapturekit/src/config.rs b/crates/scap-screencapturekit/src/config.rs
index aaebd3bca5..5787959c70 100644
--- a/crates/scap-screencapturekit/src/config.rs
+++ b/crates/scap-screencapturekit/src/config.rs
@@ -31,6 +31,10 @@ impl StreamCfgBuilder {
         });
     }
 
+    pub fn set_captures_audio(&mut self, captures_audio: bool) {
+        self.0.set_captures_audio(captures_audio);
+    }
+
     /// Logical width of the capture area
     pub fn with_width(mut self, width: usize) -> Self {
         self.set_width(width);
@@ -54,6 +58,11 @@ impl StreamCfgBuilder {
         self
     }
 
+    pub fn with_captures_audio(mut self, captures_audio: bool) -> Self {
+        self.set_captures_audio(captures_audio);
+        self
+    }
+
     pub fn with_fps(mut self, fps: f32) -> Self {
         self.set_fps(fps);
         self
diff --git a/crates/scap-screencapturekit/src/lib.rs b/crates/scap-screencapturekit/src/lib.rs
index 7db9fac845..8bf42a8849 100644
--- a/crates/scap-screencapturekit/src/lib.rs
+++ b/crates/scap-screencapturekit/src/lib.rs
@@ -2,8 +2,6 @@
 
 mod capture;
 mod config;
-mod targets;
 
 pub use capture::{AudioFrame, Capturer, CapturerBuilder, Frame, VideoFrame};
 pub use config::StreamCfgBuilder;
-pub use targets::{Display, Window};
diff --git a/crates/scap-screencapturekit/src/targets.rs b/crates/scap-screencapturekit/src/targets.rs
deleted file mode 100644
index be8aee9afb..0000000000
--- a/crates/scap-screencapturekit/src/targets.rs
+++ /dev/null
@@ -1,200 +0,0 @@
-use cidre::{arc, cg, ns, sc};
-use objc2::{MainThreadMarker, rc::Retained};
-use objc2_app_kit::NSScreen;
-use objc2_foundation::{NSArray, NSNumber, ns_string};
-
-#[derive(Clone, Debug)]
-pub struct Display {
-    inner: arc::R<sc::Display>,
-    name: String,
-}
-
-impl Display {
-    pub fn name(&self) -> &String {
-        &self.name
-    }
-
-    pub fn inner(&self) -> &sc::Display {
-        self.inner.as_ref()
-    }
-
-    /// Logical width of the display in pixels
-    pub fn width(&self) -> usize {
-        self.inner().width() as usize
-    }
-
-    /// Logical height of the display in pixels
-    pub fn height(&self) -> usize {
-        self.inner().height() as usize
-    }
-
-    pub async fn list() -> Result<Vec<Self>, arc::R<ns::Error>> {
-        let content = sc::ShareableContent::current().await?;
-
-        // SAFETY: NSScreen::screens is callable from any thread
-        let ns_screens = NSScreen::screens(unsafe { MainThreadMarker::new_unchecked() });
-
-        let displays = content
-            .displays()
-            .iter()
-            .filter_map(|display| {
-                let id = display.display_id();
-
-                let ns_screen = ns_screens.iter().find(|ns_screen| {
-                    ns_screen
-                        .deviceDescription()
-                        .objectForKey(ns_string!("NSScreenNumber"))
-                        .and_then(|v| v.downcast_ref::<NSNumber>().map(|v| v.as_u32()))
-                        .map(|v| v == id.0)
-                        .unwrap_or(false)
-                })?;
-
-                let name = unsafe { ns_screen.localizedName() }.to_string();
-
-                Some(Self {
-                    inner: display.retained(),
-                    name,
-                })
-            })
-            .collect::<Vec<_>>();
-
-        Ok(displays)
-    }
-
-    pub async fn primary() -> Option<Self> {
-        let id = cg::DirectDisplayId::main();
-
-        let content = sc::ShareableContent::current().await.ok()?;
-
-        let inner = content
-            .displays()
-            .iter()
-            .find(|d| d.display_id() == id)?
-            .retained();
-
-        Some(Self {
-            inner,
-            name: get_display_name(id, None)?,
-        })
-    }
-
-    pub async fn from_id(id: cg::DirectDisplayId) -> Option<Self> {
-        let content = sc::ShareableContent::current().await.ok()?;
-
-        let inner = content
-            .displays()
-            .iter()
-            .find(|d| d.display_id() == id)?
-            .retained();
-
-        Some(Self {
-            inner,
-            name: get_display_name(id, None)?,
-        })
-    }
-
-    pub fn as_content_filter(&self) -> arc::R<sc::ContentFilter> {
-        self.as_content_filter_excluding_windows(vec![])
-    }
-
-    pub fn as_content_filter_excluding_windows(
-        &self,
-        windows: Vec<Window>,
-    ) -> arc::R<sc::ContentFilter> {
-        let excluded_windows = ns::Array::from_slice_retained(
-            windows
-                .into_iter()
-                .map(|win| win.inner)
-                .collect::<Vec<_>>()
-                .as_slice(),
-        );
-
-        sc::ContentFilter::with_display_excluding_windows(&self.inner, &excluded_windows)
-    }
-}
-
-fn get_display_name(
-    id: cg::DirectDisplayId,
-    screens: Option<Retained<NSArray<NSScreen>>>,
-) -> Option<String> {
-    // SAFETY: NSScreen::screens is callable from any thread
-    let screens =
-        screens.unwrap_or_else(|| NSScreen::screens(unsafe { MainThreadMarker::new_unchecked() }));
-
-    let ns_screen = screens.iter().find(|ns_screen| {
-        ns_screen
-            .deviceDescription()
-            .objectForKey(ns_string!("NSScreenNumber"))
-            .and_then(|v| v.downcast_ref::<NSNumber>().map(|v| v.as_u32()))
-            .map(|v| v == id.0)
-            .unwrap_or(false)
-    })?;
-
-    Some(unsafe { ns_screen.localizedName() }.to_string())
-}
-
-#[derive(Clone, Debug)]
-pub struct Window {
-    inner: arc::R<sc::Window>,
-    title: Option<String>,
-}
-
-impl Window {
-    pub fn title(&self) -> Option<&String> {
-        self.title.as_ref()
-    }
-
-    pub fn inner(&self) -> &sc::Window {
-        self.inner.as_ref()
-    }
-
-    /// Logical width of the window in pixels.
-    pub fn width(&self) -> usize {
-        self.inner.frame().size.width as usize
-    }
-
-    /// Logical height of the window in pixels.
-    pub fn height(&self) -> usize {
-        self.inner.frame().size.height as usize
-    }
-
-    fn from_sc(window: &sc::Window) -> Self {
-        Self {
-            inner: window.retained(),
-            title: window.title().map(|s| s.to_string()),
-        }
-    }
-
-    pub async fn list() -> Result<Vec<Self>, arc::R<ns::Error>> {
-        let content = sc::ShareableContent::current().await?;
-
-        let windows = content
-            .windows()
-            .iter()
-            .map(Self::from_sc)
-            .collect::<Vec<_>>();
-
-        Ok(windows)
-    }
-
-    pub fn as_content_filter(&self) -> arc::R<sc::ContentFilter> {
-        sc::ContentFilter::with_desktop_independent_window(self.inner.as_ref())
-    }
-}
-
-#[cfg(debug_assertions)]
-mod test {
-    use super::*;
-
-    fn assert_send<T: Send>() {}
-    fn assert_send_val<T: Send>(_: T) {}
-
-    #[allow(dead_code)]
-    fn ensure_send() {
-        assert_send::<Display>();
-        assert_send_val(Display::list());
-
-        assert_send::<Window>();
-        assert_send_val(Window::list());
-    }
-}
diff --git a/crates/video-decode/Cargo.toml b/crates/video-decode/Cargo.toml
index cbb6c9fd93..d6bfc085ca 100644
--- a/crates/video-decode/Cargo.toml
+++ b/crates/video-decode/Cargo.toml
@@ -8,7 +8,6 @@ workspace = true
 
 [dependencies]
 ffmpeg.workspace = true
-ffmpeg-sys-next.workspace = true
 ffmpeg-hw-device = { path = "../ffmpeg-hw-device" }
 tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
 tracing = "0.1.41"
diff --git a/crates/video-decode/src/ffmpeg.rs b/crates/video-decode/src/ffmpeg.rs
index b1844e5771..29e39446b9 100644
--- a/crates/video-decode/src/ffmpeg.rs
+++ b/crates/video-decode/src/ffmpeg.rs
@@ -1,10 +1,11 @@
 use ffmpeg::{
     codec as avcodec,
     format::{self as avformat, context::input::PacketIter},
-    frame as avframe, util as avutil,
+    frame as avframe,
+    sys::{AVHWDeviceType, EAGAIN},
+    util as avutil,
 };
 use ffmpeg_hw_device::{CodecContextExt, HwDevice};
-use ffmpeg_sys_next::{AVHWDeviceType, EAGAIN};
 use std::path::PathBuf;
 use tracing::debug;
 
diff --git a/packages/ui-solid/src/auto-imports.d.ts b/packages/ui-solid/src/auto-imports.d.ts
index 054b44f3c1..792d482761 100644
--- a/packages/ui-solid/src/auto-imports.d.ts
+++ b/packages/ui-solid/src/auto-imports.d.ts
@@ -56,10 +56,7 @@ declare global {
 	const IconCapUpload: typeof import("~icons/cap/upload.jsx")["default"];
 	const IconCapZoomIn: typeof import("~icons/cap/zoom-in.jsx")["default"];
 	const IconCapZoomOut: typeof import("~icons/cap/zoom-out.jsx")["default"];
-	const IconFa6SolidDisplay: typeof import("~icons/fa6-solid/display.jsx")["default"];
 	const IconHugeiconsEaseCurveControlPoints: typeof import("~icons/hugeicons/ease-curve-control-points.jsx")["default"];
-	const IconIcBaselineMonitor: typeof import("~icons/ic/baseline-monitor.jsx")["default"];
-	const IconIcRoundSearch: typeof import("~icons/ic/round-search.jsx")["default"];
 	const IconLucideAppWindowMac: typeof import("~icons/lucide/app-window-mac.jsx")["default"];
 	const IconLucideBell: typeof import("~icons/lucide/bell.jsx")["default"];
 	const IconLucideBug: typeof import("~icons/lucide/bug.jsx")["default"];
@@ -84,9 +81,6 @@ declare global {
 	const IconLucideUnplug: typeof import("~icons/lucide/unplug.jsx")["default"];
 	const IconLucideVolume2: typeof import("~icons/lucide/volume2.jsx")["default"];
 	const IconLucideVolumeX: typeof import("~icons/lucide/volume-x.jsx")["default"];
-	const IconLucideX: typeof import("~icons/lucide/x.jsx")["default"];
-	const IconMaterialSymbolsLightScreenshotFrame2: typeof import("~icons/material-symbols-light/screenshot-frame2.jsx")["default"];
-	const IconMaterialSymbolsLightScreenshotFrame2MaterialSymbolsScreenshotFrame2Rounded: typeof import("~icons/material-symbols-light/screenshot-frame2-material-symbols-screenshot-frame2-rounded.jsx")["default"];
 	const IconMaterialSymbolsScreenshotFrame2Rounded: typeof import("~icons/material-symbols/screenshot-frame2-rounded.jsx")["default"];
 	const IconMdiMonitor: typeof import("~icons/mdi/monitor.jsx")["default"];
 	const IconPhMonitorBold: typeof import("~icons/ph/monitor-bold.jsx")["default"];
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 44e8dc1708..7811110fbf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -97,7 +97,7 @@ importers:
         version: 1.5.1(solid-js@1.9.6)
       '@solid-primitives/storage':
         specifier: ^4.0.0
-        version: 4.3.2(@tauri-apps/plugin-store@2.3.0)(solid-js@1.9.6)
+        version: 4.3.2(@tauri-apps/plugin-store@2.4.0)(solid-js@1.9.6)
       '@solid-primitives/timer':
         specifier: ^1.3.9
         version: 1.4.1(solid-js@1.9.6)
@@ -135,8 +135,8 @@ importers:
         specifier: ^2.3.0
         version: 2.3.0
       '@tauri-apps/plugin-opener':
-        specifier: ^2.4.0
-        version: 2.4.0
+        specifier: ^2.5.0
+        version: 2.5.0
       '@tauri-apps/plugin-os':
         specifier: ^2.3.0
         version: 2.3.0
@@ -147,8 +147,8 @@ importers:
         specifier: ^2.3.0
         version: 2.3.0
       '@tauri-apps/plugin-store':
-        specifier: ^2.3.0
-        version: 2.3.0
+        specifier: ^2.4.0
+        version: 2.4.0
       '@tauri-apps/plugin-updater':
         specifier: ^2.9.0
         version: 2.9.0
@@ -325,7 +325,7 @@ importers:
         version: 1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(solid-js@1.9.6)(storybook@8.6.12(prettier@3.5.3))
       storybook-solidjs-vite:
         specifier: ^1.0.0-beta.2
-        version: 1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(solid-js@1.9.6)(storybook@8.6.12(prettier@3.5.3))(vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1))
+        version: 1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(esbuild@0.25.4)(rollup@4.40.2)(solid-js@1.9.6)(storybook@8.6.12(prettier@3.5.3))(vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1))
       typescript:
         specifier: ^5.8.3
         version: 5.8.3
@@ -3383,10 +3383,16 @@ packages:
     resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
 
+  '@jridgewell/gen-mapping@0.3.13':
+    resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
   '@jridgewell/gen-mapping@0.3.8':
     resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
     engines: {node: '>=6.0.0'}
 
+  '@jridgewell/remapping@2.3.5':
+    resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
   '@jridgewell/resolve-uri@3.1.2':
     resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
     engines: {node: '>=6.0.0'}
@@ -3401,9 +3407,15 @@ packages:
   '@jridgewell/sourcemap-codec@1.5.0':
     resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
 
+  '@jridgewell/sourcemap-codec@1.5.5':
+    resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
   '@jridgewell/trace-mapping@0.3.25':
     resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
 
+  '@jridgewell/trace-mapping@0.3.30':
+    resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==}
+
   '@jridgewell/trace-mapping@0.3.9':
     resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
 
@@ -5819,10 +5831,10 @@ packages:
       react-dom:
         optional: true
 
-  '@storybook/builder-vite@9.2.0-alpha.3':
-    resolution: {integrity: sha512-QkZA/9pOmcbES9COYQLPzgqPBPjEeoFNnwoz1uAYa9k8rqCGHDKi2nnGDYFbikvpJs3BougV0vrd/vY37CmL5Q==}
+  '@storybook/builder-vite@10.0.0-beta.0':
+    resolution: {integrity: sha512-IGd75RO4YR7ofOZWqND2dYHrKLjwN05+mM1xfrucb2SoQzjbNXvFNVVxtOCBmqsarjmrMKLKHuRgFzU41vs43g==}
     peerDependencies:
-      storybook: ^9.2.0-alpha.3
+      storybook: ^10.0.0-beta.0
       vite: ^5.0.0 || ^6.0.0 || ^7.0.0
 
   '@storybook/core@8.6.12':
@@ -5833,16 +5845,29 @@ packages:
       prettier:
         optional: true
 
+  '@storybook/csf-plugin@10.0.0-beta.0':
+    resolution: {integrity: sha512-2vFHbbnp/yGWt4p53OO5swDVZzmOp3HkJEbhc1zE9BW3YcX0mG8Gh2sd7K0+Io04uoupwJpawTz8VDdKfYkJUA==}
+    peerDependencies:
+      esbuild: '*'
+      rollup: '*'
+      storybook: ^10.0.0-beta.0
+      vite: '*'
+      webpack: '*'
+    peerDependenciesMeta:
+      esbuild:
+        optional: true
+      rollup:
+        optional: true
+      vite:
+        optional: true
+      webpack:
+        optional: true
+
   '@storybook/csf-plugin@8.6.12':
     resolution: {integrity: sha512-6s8CnP1aoKPb3XtC0jRLUp8M5vTA8RhGAwQDKUsFpCC7g89JR9CaKs9FY2ZSzsNbjR15uASi7b3K8BzeYumYQg==}
     peerDependencies:
       storybook: ^8.6.12
 
-  '@storybook/csf-plugin@9.2.0-alpha.3':
-    resolution: {integrity: sha512-pAoSleGEUkl2Hx2l17GkA+DZMRdrUAn/oiTrig5dGJaVRlEvTsd4c/e/wkiEPBxLnTXsJTdcEANxTMBByhybCA==}
-    peerDependencies:
-      storybook: ^9.2.0-alpha.3
-
   '@storybook/docs-tools@8.6.12':
     resolution: {integrity: sha512-bOmTNJE4FM6+3/1hPQVgha8cLxX2Nyi/4Z+tYq0pCDEyQqGRTD2KXre9XsSCnziRaGxiW80OrKkmqWY8Otuu4A==}
     peerDependencies:
@@ -6005,6 +6030,9 @@ packages:
   '@tauri-apps/api@2.7.0':
     resolution: {integrity: sha512-v7fVE8jqBl8xJFOcBafDzXFc8FnicoH3j8o8DNNs0tHuEBmXUDqrCOAzMRX0UkfpwqZLqvrvK0GNQ45DfnoVDg==}
 
+  '@tauri-apps/api@2.8.0':
+    resolution: {integrity: sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==}
+
   '@tauri-apps/cli-darwin-arm64@1.6.3':
     resolution: {integrity: sha512-fQN6IYSL8bG4NvkdKE4sAGF4dF/QqqQq4hOAU+t8ksOzHJr0hUlJYfncFeJYutr/MMkdF7hYKadSb0j5EE9r0A==}
     engines: {node: '>= 10'}
@@ -6159,8 +6187,8 @@ packages:
   '@tauri-apps/plugin-notification@2.3.0':
     resolution: {integrity: sha512-QDwXo9VzAlH97c0veuf19TZI6cRBPfJDl2O6hNEDvI66j60lOO9z+PL6MJrj8A6Y+t55r7mGhe3rQWLmOre2HA==}
 
-  '@tauri-apps/plugin-opener@2.4.0':
-    resolution: {integrity: sha512-43VyN8JJtvKWJY72WI/KNZszTpDpzHULFxQs0CJBIYUdCRowQ6Q1feWTDb979N7nldqSuDOaBupZ6wz2nvuWwQ==}
+  '@tauri-apps/plugin-opener@2.5.0':
+    resolution: {integrity: sha512-B0LShOYae4CZjN8leiNDbnfjSrTwoZakqKaWpfoH6nXiJwt6Rgj6RnVIffG3DoJiKsffRhMkjmBV9VeilSb4TA==}
 
   '@tauri-apps/plugin-os@2.3.0':
     resolution: {integrity: sha512-dm3bDsMuUngpIQdJ1jaMkMfyQpHyDcaTIKTFaAMHoKeUd+Is3UHO2uzhElr6ZZkfytIIyQtSVnCWdW2Kc58f3g==}
@@ -6171,8 +6199,8 @@ packages:
   '@tauri-apps/plugin-shell@2.3.0':
     resolution: {integrity: sha512-6GIRxO2z64uxPX4CCTuhQzefvCC0ew7HjdBhMALiGw74vFBDY95VWueAHOHgNOMV4UOUAFupyidN9YulTe5xlA==}
 
-  '@tauri-apps/plugin-store@2.3.0':
-    resolution: {integrity: sha512-mre8er0nXPhyEWQzWCpUd+UnEoBQYcoA5JYlwpwOV9wcxKqlXTGfminpKsE37ic8NUb2BIZqf0QQ9/U3ib2+/A==}
+  '@tauri-apps/plugin-store@2.4.0':
+    resolution: {integrity: sha512-PjBnlnH6jyI71MGhrPaxUUCsOzc7WO1mbc4gRhME0m2oxLgCqbksw6JyeKQimuzv4ysdpNO3YbmaY2haf82a3A==}
 
   '@tauri-apps/plugin-updater@2.9.0':
     resolution: {integrity: sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg==}
@@ -11316,6 +11344,10 @@ packages:
     resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
     engines: {node: '>=12'}
 
+  picomatch@4.0.3:
+    resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+    engines: {node: '>=12'}
+
   pify@2.3.0:
     resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
     engines: {node: '>=0.10.0'}
@@ -13149,6 +13181,10 @@ packages:
     resolution: {integrity: sha512-3n7YA46rROb3zSj8fFxtxC/PqoyvYQ0llwz9wtUPUutr9ig09C8gGo5CWCwHrUzlqC1LLR43kxp5vEIyH1ac1w==}
     engines: {node: '>=18.12.0'}
 
+  unplugin@2.3.8:
+    resolution: {integrity: sha512-lkaSIlxceytPyt9yfb1h7L9jDFqwMqvUZeGsKB7Z8QrvAO3xZv2S+xMQQYzxk0AGJHcQhbcvhKEstrMy99jnuQ==}
+    engines: {node: '>=18.12.0'}
+
   unrs-resolver@1.7.2:
     resolution: {integrity: sha512-BBKpaylOW8KbHsu378Zky/dGh4ckT/4NW/0SHRABdqRLcQJ2dAOjDo9g97p04sWflm0kqPqpUatxReNV/dqI5A==}
 
@@ -16115,12 +16151,22 @@ snapshots:
       '@types/yargs': 17.0.33
       chalk: 4.1.2
 
+  '@jridgewell/gen-mapping@0.3.13':
+    dependencies:
+      '@jridgewell/sourcemap-codec': 1.5.5
+      '@jridgewell/trace-mapping': 0.3.30
+
   '@jridgewell/gen-mapping@0.3.8':
     dependencies:
       '@jridgewell/set-array': 1.2.1
       '@jridgewell/sourcemap-codec': 1.5.0
       '@jridgewell/trace-mapping': 0.3.25
 
+  '@jridgewell/remapping@2.3.5':
+    dependencies:
+      '@jridgewell/gen-mapping': 0.3.13
+      '@jridgewell/trace-mapping': 0.3.30
+
   '@jridgewell/resolve-uri@3.1.2': {}
 
   '@jridgewell/set-array@1.2.1': {}
@@ -16132,11 +16178,18 @@ snapshots:
 
   '@jridgewell/sourcemap-codec@1.5.0': {}
 
+  '@jridgewell/sourcemap-codec@1.5.5': {}
+
   '@jridgewell/trace-mapping@0.3.25':
     dependencies:
       '@jridgewell/resolve-uri': 3.1.2
       '@jridgewell/sourcemap-codec': 1.5.0
 
+  '@jridgewell/trace-mapping@0.3.30':
+    dependencies:
+      '@jridgewell/resolve-uri': 3.1.2
+      '@jridgewell/sourcemap-codec': 1.5.5
+
   '@jridgewell/trace-mapping@0.3.9':
     dependencies:
       '@jridgewell/resolve-uri': 3.1.2
@@ -19039,12 +19092,12 @@ snapshots:
       '@solid-primitives/utils': 6.3.2(solid-js@1.9.6)
       solid-js: 1.9.6
 
-  '@solid-primitives/storage@4.3.2(@tauri-apps/plugin-store@2.3.0)(solid-js@1.9.6)':
+  '@solid-primitives/storage@4.3.2(@tauri-apps/plugin-store@2.4.0)(solid-js@1.9.6)':
     dependencies:
       '@solid-primitives/utils': 6.3.1(solid-js@1.9.6)
       solid-js: 1.9.6
     optionalDependencies:
-      '@tauri-apps/plugin-store': 2.3.0
+      '@tauri-apps/plugin-store': 2.4.0
 
   '@solid-primitives/timer@1.4.1(solid-js@1.9.6)':
     dependencies:
@@ -19218,12 +19271,16 @@ snapshots:
       react: 19.1.0
       react-dom: 19.1.0(react@19.1.0)
 
-  '@storybook/builder-vite@9.2.0-alpha.3(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1))':
+  '@storybook/builder-vite@10.0.0-beta.0(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1))':
     dependencies:
-      '@storybook/csf-plugin': 9.2.0-alpha.3(storybook@8.6.12(prettier@3.5.3))
+      '@storybook/csf-plugin': 10.0.0-beta.0(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1))
       storybook: 8.6.12(prettier@3.5.3)
       ts-dedent: 2.2.0
       vite: 6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)
+    transitivePeerDependencies:
+      - esbuild
+      - rollup
+      - webpack
 
   '@storybook/core@8.6.12(prettier@3.5.3)(storybook@8.6.12(prettier@3.5.3))':
     dependencies:
@@ -19246,12 +19303,16 @@ snapshots:
       - supports-color
       - utf-8-validate
 
-  '@storybook/csf-plugin@8.6.12(storybook@8.6.12(prettier@3.5.3))':
+  '@storybook/csf-plugin@10.0.0-beta.0(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1))':
     dependencies:
       storybook: 8.6.12(prettier@3.5.3)
-      unplugin: 1.16.1
+      unplugin: 2.3.8
+    optionalDependencies:
+      esbuild: 0.25.4
+      rollup: 4.40.2
+      vite: 6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)
 
-  '@storybook/csf-plugin@9.2.0-alpha.3(storybook@8.6.12(prettier@3.5.3))':
+  '@storybook/csf-plugin@8.6.12(storybook@8.6.12(prettier@3.5.3))':
     dependencies:
       storybook: 8.6.12(prettier@3.5.3)
       unplugin: 1.16.1
@@ -19462,6 +19523,8 @@ snapshots:
 
   '@tauri-apps/api@2.7.0': {}
 
+  '@tauri-apps/api@2.8.0': {}
+
   '@tauri-apps/cli-darwin-arm64@1.6.3':
     optional: true
 
@@ -19578,9 +19641,9 @@ snapshots:
     dependencies:
       '@tauri-apps/api': 2.7.0
 
-  '@tauri-apps/plugin-opener@2.4.0':
+  '@tauri-apps/plugin-opener@2.5.0':
     dependencies:
-      '@tauri-apps/api': 2.7.0
+      '@tauri-apps/api': 2.8.0
 
   '@tauri-apps/plugin-os@2.3.0':
     dependencies:
@@ -19594,9 +19657,9 @@ snapshots:
     dependencies:
       '@tauri-apps/api': 2.7.0
 
-  '@tauri-apps/plugin-store@2.3.0':
+  '@tauri-apps/plugin-store@2.4.0':
     dependencies:
-      '@tauri-apps/api': 2.7.0
+      '@tauri-apps/api': 2.8.0
 
   '@tauri-apps/plugin-updater@2.9.0':
     dependencies:
@@ -22215,8 +22278,8 @@ snapshots:
       '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3)
       eslint: 8.57.1
       eslint-import-resolver-node: 0.3.9
-      eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1)
-      eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
+      eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1)
+      eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
       eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
       eslint-plugin-react: 7.37.5(eslint@8.57.1)
       eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1)
@@ -22263,6 +22326,21 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1):
+    dependencies:
+      '@nolyfill/is-core-module': 1.0.39
+      debug: 4.4.0(supports-color@5.5.0)
+      eslint: 8.57.1
+      get-tsconfig: 4.10.0
+      is-bun-module: 2.0.0
+      stable-hash: 0.0.5
+      tinyglobby: 0.2.13
+      unrs-resolver: 1.7.2
+    optionalDependencies:
+      eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
+    transitivePeerDependencies:
+      - supports-color
+
   eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)):
     dependencies:
       '@nolyfill/is-core-module': 1.0.39
@@ -22293,14 +22371,14 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
+  eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
     dependencies:
       debug: 3.2.7
     optionalDependencies:
       '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3)
       eslint: 8.57.1
       eslint-import-resolver-node: 0.3.9
-      eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1)
+      eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1)
     transitivePeerDependencies:
       - supports-color
 
@@ -22326,7 +22404,7 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
+  eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
     dependencies:
       '@rtsao/scc': 1.1.0
       array-includes: 3.1.8
@@ -22337,7 +22415,7 @@ snapshots:
       doctrine: 2.1.0
       eslint: 8.57.1
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
+      eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
       hasown: 2.0.2
       is-core-module: 2.16.1
       is-glob: 4.0.3
@@ -26128,6 +26206,8 @@ snapshots:
 
   picomatch@4.0.2: {}
 
+  picomatch@4.0.3: {}
+
   pify@2.3.0: {}
 
   pirates@4.0.7: {}
@@ -27432,9 +27512,9 @@ snapshots:
 
   stoppable@1.1.0: {}
 
-  storybook-solidjs-vite@1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(solid-js@1.9.6)(storybook@8.6.12(prettier@3.5.3))(vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)):
+  storybook-solidjs-vite@1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(esbuild@0.25.4)(rollup@4.40.2)(solid-js@1.9.6)(storybook@8.6.12(prettier@3.5.3))(vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1)):
     dependencies:
-      '@storybook/builder-vite': 9.2.0-alpha.3(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1))
+      '@storybook/builder-vite': 10.0.0-beta.0(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1))
       '@storybook/types': 9.0.0-alpha.1(storybook@8.6.12(prettier@3.5.3))
       magic-string: 0.30.17
       solid-js: 1.9.6
@@ -27444,6 +27524,9 @@ snapshots:
       vite-plugin-solid: 2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.39.0)(yaml@2.8.1))
     transitivePeerDependencies:
       - '@storybook/test'
+      - esbuild
+      - rollup
+      - webpack
 
   storybook-solidjs@1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(solid-js@1.9.6)(storybook@8.6.12(prettier@3.5.3)):
     dependencies:
@@ -28338,6 +28421,13 @@ snapshots:
       picomatch: 4.0.2
       webpack-virtual-modules: 0.6.2
 
+  unplugin@2.3.8:
+    dependencies:
+      '@jridgewell/remapping': 2.3.5
+      acorn: 8.15.0
+      picomatch: 4.0.3
+      webpack-virtual-modules: 0.6.2
+
   unrs-resolver@1.7.2:
     dependencies:
       napi-postinstall: 0.2.3
