Revert "Use livekit's Rust SDK instead of their swift SDK (#13343)" (#20809)

Kirill Bulatov created

Issues found:

* audio does not work well with various set-ups using USB
* switching audio during initial join may leave the client with no audio
at all
* audio streaming is done on the main thread, beachballing certain
set-ups
* worse screenshare quality (seems that there's no dynamic scaling
anymore, compared to the Swift SDK)

This reverts commit 1235d0808ed7697874085fc8528f22aec988c85e.

Release Notes:

- N/A

Change summary

.cargo/config.toml                                                             |   6 
Cargo.lock                                                                     | 363 
Cargo.toml                                                                     |   6 
crates/call/Cargo.toml                                                         |   1 
crates/call/src/call.rs                                                        |   9 
crates/call/src/participant.rs                                                 |  28 
crates/call/src/room.rs                                                        | 804 
crates/collab/src/tests.rs                                                     |   3 
crates/collab/src/tests/channel_guest_tests.rs                                 |  24 
crates/collab/src/tests/following_tests.rs                                     |  13 
crates/collab/src/tests/integration_tests.rs                                   |  27 
crates/collab/src/tests/test_server.rs                                         |   6 
crates/collab_ui/src/collab_panel.rs                                           |   5 
crates/gpui/build.rs                                                           |   1 
crates/gpui/src/app.rs                                                         |  11 
crates/gpui/src/app/test_context.rs                                            |  10 
crates/gpui/src/geometry.rs                                                    |   5 
crates/gpui/src/platform.rs                                                    |  26 
crates/gpui/src/platform/linux.rs                                              |   2 
crates/gpui/src/platform/linux/platform.rs                                     |  12 
crates/gpui/src/platform/mac.rs                                                |   5 
crates/gpui/src/platform/mac/platform.rs                                       |  14 
crates/gpui/src/platform/mac/screen_capture.rs                                 | 239 
crates/gpui/src/platform/test.rs                                               |   2 
crates/gpui/src/platform/test/platform.rs                                      |  58 
crates/gpui/src/platform/windows.rs                                            |   2 
crates/gpui/src/platform/windows/platform.rs                                   |   8 
crates/http_client/Cargo.toml                                                  |   2 
crates/live_kit_client/Cargo.toml                                              |  29 
crates/live_kit_client/LiveKitBridge/Package.resolved                          |  52 
crates/live_kit_client/LiveKitBridge/Package.swift                             |  27 
crates/live_kit_client/LiveKitBridge/README.md                                 |   3 
crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift | 383 
crates/live_kit_client/build.rs                                                | 185 
crates/live_kit_client/examples/test_app.rs                                    | 494 
crates/live_kit_client/src/live_kit_client.rs                                  | 410 
crates/live_kit_client/src/prod.rs                                             | 981 
crates/live_kit_client/src/remote_video_track_view.rs                          |  61 
crates/live_kit_client/src/test.rs                                             | 843 
crates/live_kit_client/src/test/participant.rs                                 | 111 
crates/live_kit_client/src/test/publication.rs                                 | 116 
crates/live_kit_client/src/test/track.rs                                       | 201 
crates/live_kit_client/src/test/webrtc.rs                                      | 136 
crates/media/Cargo.toml                                                        |   1 
crates/media/src/media.rs                                                      |  11 
crates/title_bar/src/collab.rs                                                 |  63 
crates/workspace/src/shared_screen.rs                                          |  52 
crates/workspace/src/workspace.rs                                              |  13 
48 files changed, 2,927 insertions(+), 2,937 deletions(-)

Detailed changes

.cargo/config.toml 🔗

@@ -13,12 +13,6 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"]
 linker = "clang"
 rustflags = ["-C", "link-arg=-fuse-ld=mold"]
 
-[target.aarch64-apple-darwin]
-rustflags = ["-C", "link-args=-Objc -all_load"]
-
-[target.x86_64-apple-darwin]
-rustflags = ["-C", "link-args=-Objc -all_load"]
-
 # This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes
 [target.'cfg(target_os = "windows")']
 rustflags = ["--cfg", "windows_slim_errors"]

Cargo.lock 🔗

@@ -10,7 +10,7 @@ dependencies = [
  "auto_update",
  "editor",
  "extension_host",
- "futures 0.3.31",
+ "futures 0.3.30",
  "gpui",
  "language",
  "lsp",
@@ -23,13 +23,19 @@ dependencies = [
 
 [[package]]
 name = "addr2line"
-version = "0.24.2"
+version = "0.24.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375"
 dependencies = [
- "gimli 0.31.1",
+ "gimli 0.31.0",
 ]
 
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
 [[package]]
 name = "adler2"
 version = "2.0.0"
@@ -94,8 +100,8 @@ dependencies = [
  "miow",
  "parking_lot",
  "piper",
- "polling 3.7.4",
- "regex-automata 0.4.9",
+ "polling 3.7.3",
+ "regex-automata 0.4.7",
  "rustix-openpty",
  "serde",
  "signal-hook",
@@ -118,9 +124,9 @@ checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
 
 [[package]]
 name = "allocator-api2"
-version = "0.2.20"
+version = "0.2.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9"
+checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
 
 [[package]]
 name = "alsa"
@@ -186,9 +192,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
 
 [[package]]
 name = "anstream"
-version = "0.6.18"
+version = "0.6.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
 dependencies = [
  "anstyle",
  "anstyle-parse",
@@ -201,36 +207,36 @@ dependencies = [
 
 [[package]]
 name = "anstyle"
-version = "1.0.10"
+version = "1.0.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
 
 [[package]]
 name = "anstyle-parse"
-version = "0.2.6"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
 dependencies = [
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle-query"
-version = "1.1.2"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
 dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.6"
+version = "3.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
+checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
 dependencies = [
  "anstyle",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -239,13 +245,13 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "chrono",
- "futures 0.3.31",
+ "futures 0.3.30",
  "http_client",
  "schemars",
  "serde",
  "serde_json",
  "strum 0.25.0",
- "thiserror 1.0.69",
+ "thiserror",
  "util",
 ]
 
@@ -272,9 +278,9 @@ dependencies = [
 
 [[package]]
 name = "arbitrary"
-version = "1.4.1"
+version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
+checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
 
 [[package]]
 name = "arg_enum_proc_macro"
@@ -295,9 +301,9 @@ checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
 
 [[package]]
 name = "arrayref"
-version = "0.3.9"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
+checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a"
 
 [[package]]
 name = "arrayvec"
@@ -390,7 +396,7 @@ dependencies = [
  "env_logger 0.11.5",
  "feature_flags",
  "fs",
- "futures 0.3.31",
+ "futures 0.3.30",
  "fuzzy",
  "globset",
  "gpui",
@@ -457,7 +463,7 @@ dependencies = [
  "collections",
  "derive_more",
  "extension",
- "futures 0.3.31",
+ "futures 0.3.30",
  "gpui",
  "language",
  "language_model",
@@ -567,14 +573,14 @@ dependencies = [
 
 [[package]]
 name = "async-executor"
-version = "1.13.1"
+version = "1.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec"
+checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7"
 dependencies = [
  "async-task",
  "concurrent-queue",
- "fastrand 2.2.0",
- "futures-lite 2.5.0",
+ "fastrand 2.1.1",
+ "futures-lite 2.3.0",
  "slab",
 ]
 
@@ -598,7 +604,7 @@ checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a"
 dependencies = [
  "async-lock 3.4.0",
  "blocking",
- "futures-lite 2.5.0",
+ "futures-lite 2.3.0",
 ]
 
 [[package]]
@@ -609,10 +615,10 @@ checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
 dependencies = [
  "async-channel 2.3.1",
  "async-executor",
- "async-io 2.4.0",
+ "async-io 2.3.4",
  "async-lock 3.4.0",
  "blocking",
- "futures-lite 2.5.0",
+ "futures-lite 2.3.0",
  "once_cell",
 ]
 
@@ -638,18 +644,18 @@ dependencies = [
 
 [[package]]
 name = "async-io"
-version = "2.4.0"
+version = "2.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
+checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8"
 dependencies = [
  "async-lock 3.4.0",
  "cfg-if",
  "concurrent-queue",
  "futures-io",
- "futures-lite 2.5.0",
+ "futures-lite 2.3.0",
  "parking",
- "polling 3.7.4",
- "rustix 0.38.40",
+ "polling 3.7.3",
+ "rustix 0.38.35",
  "slab",
  "tracing",
  "windows-sys 0.59.0",
@@ -683,7 +689,7 @@ checksum = "9343dc5acf07e79ff82d0c37899f079db3534d99f189a1837c8e549c99405bec"
 dependencies = [
  "futures-util",
  "native-tls",
- "thiserror 1.0.69",
+ "thiserror",
  "url",
 ]
 
@@ -704,9 +710,9 @@ version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
 dependencies = [
- "async-io 2.4.0",
+ "async-io 2.3.4",
  "blocking",
- "futures-lite 2.5.0",
+ "futures-lite 2.3.0",
 ]
 
 [[package]]
@@ -714,7 +720,7 @@ name = "async-pipe"
 version = "0.1.3"
 source = "git+https://github.com/zed-industries/async-pipe-rs?rev=82d00a04211cf4e1236029aa03e6b6ce2a74c553#82d00a04211cf4e1236029aa03e6b6ce2a74c553"
 dependencies = [
- "futures 0.3.31",
+ "futures 0.3.30",
  "log",
 ]
 
@@ -731,27 +737,28 @@ dependencies = [
  "cfg-if",
  "event-listener 3.1.0",
  "futures-lite 1.13.0",
- "rustix 0.38.40",
+ "rustix 0.38.35",
  "windows-sys 0.48.0",
 ]
 
 [[package]]
 name = "async-process"
-version = "2.3.0"
+version = "2.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
+checksum = "a8a07789659a4d385b79b18b9127fc27e1a59e1e89117c78c5ea3b806f016374"
 dependencies = [
  "async-channel 2.3.1",
- "async-io 2.4.0",
+ "async-io 2.3.4",
  "async-lock 3.4.0",
  "async-signal",
  "async-task",
  "blocking",
  "cfg-if",
  "event-listener 5.3.1",
- "futures-lite 2.5.0",
- "rustix 0.38.40",
+ "futures-lite 2.3.0",
+ "rustix 0.38.35",
  "tracing",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -782,13 +789,13 @@ version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3"
 dependencies = [
- "async-io 2.4.0",
+ "async-io 2.3.4",
  "async-lock 3.4.0",
  "atomic-waker",
  "cfg-if",
  "futures-core",
  "futures-io",
- "rustix 0.38.40",
+ "rustix 0.38.35",
  "signal-hook-registry",
  "slab",
  "windows-sys 0.59.0",
@@ -796,21 +803,21 @@ dependencies = [
 
 [[package]]
 name = "async-std"
-version = "1.13.0"
+version = "1.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615"
+checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
 dependencies = [
  "async-attributes",
  "async-channel 1.9.0",
  "async-global-executor",
- "async-io 2.4.0",
- "async-lock 3.4.0",
- "async-process 2.3.0",
+ "async-io 1.13.0",
+ "async-lock 2.8.0",
+ "async-process 1.8.1",
  "crossbeam-utils",
  "futures-channel",
  "futures-core",
  "futures-io",
- "futures-lite 2.5.0",
+ "futures-lite 1.13.0",
  "gloo-timers",
  "kv-log-macro",
  "log",
@@ -824,9 +831,9 @@ dependencies = [
 
 [[package]]
 name = "async-stream"
-version = "0.3.6"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
+checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
 dependencies = [
  "async-stream-impl",
  "futures-core",
@@ -835,9 +842,9 @@ dependencies = [
 
 [[package]]
 name = "async-stream-impl"
-version = "0.3.6"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
+checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -860,7 +867,7 @@ dependencies = [
  "serde_qs 0.10.1",
  "smart-default",
  "smol_str",
- "thiserror 1.0.69",
+ "thiserror",
  "tokio",
 ]
 
@@ -908,22 +915,6 @@ dependencies = [
  "syn 2.0.87",
 ]
 
-[[package]]
-name = "async-tungstenite"
-version = "0.25.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2cca750b12e02c389c1694d35c16539f88b8bbaa5945934fdc1b41a776688589"
-dependencies = [
- "async-native-tls",
- "async-std",
- "async-tls",
- "futures-io",
- "futures-util",
- "log",
- "pin-project-lite",
- "tungstenite 0.21.0",
-]
-
 [[package]]
 name = "async-tungstenite"
 version = "0.28.0"
@@ -956,9 +947,9 @@ checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
 dependencies = [
  "async-compression",
  "crc32fast",
- "futures-lite 2.5.0",
+ "futures-lite 2.3.0",
  "pin-project",
- "thiserror 1.0.69",
+ "thiserror",
 ]
 
 [[package]]
@@ -967,7 +958,7 @@ version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233"
 dependencies = [
- "bytes 1.8.0",
+ "bytes 1.7.2",
  "futures-sink",
  "futures-util",
  "memchr",
@@ -1037,9 +1028,9 @@ dependencies = [
 
 [[package]]
 name = "autocfg"
-version = "1.4.0"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
 
 [[package]]
 name = "av1-grain"
@@ -1057,18 +1048,18 @@ dependencies = [
 
 [[package]]
 name = "avif-serialize"
-version = "0.8.2"
+version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62"
+checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2"
 dependencies = [
  "arrayvec",
 ]
 
 [[package]]
 name = "aws-config"
-version = "1.5.10"
+version = "1.5.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b49afaa341e8dd8577e1a2200468f98956d6eda50bcf4a53246cc00174ba924"
+checksum = "4e95816a168520d72c0e7680c405a5a8c1fb6a035b4bc4b9d7b0de8e1a941697"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1082,8 +1073,8 @@ dependencies = [
  "aws-smithy-runtime-api",
  "aws-smithy-types",
  "aws-types",
- "bytes 1.8.0",
- "fastrand 2.2.0",
+ "bytes 1.7.2",
+ "fastrand 2.1.1",
  "hex",
  "http 0.2.12",
  "ring",
@@ -1121,8 +1112,8 @@ dependencies = [
  "aws-smithy-runtime-api",
  "aws-smithy-types",
  "aws-types",
- "bytes 1.8.0",
- "fastrand 2.2.0",
+ "bytes 1.7.2",
+ "fastrand 2.1.1",
  "http 0.2.12",
  "http-body 0.4.6",
  "once_cell",
@@ -1147,7 +1138,7 @@ dependencies = [
  "aws-smithy-runtime-api",
  "aws-smithy-types",
  "aws-types",
- "bytes 1.8.0",
+ "bytes 1.7.2",
  "http 0.2.12",
  "once_cell",
  "regex-lite",
@@ -1156,10 +1147,11 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-s3"
-version = "1.61.0"
+version = "1.47.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e531658a0397d22365dfe26c3e1c0c8448bf6a3a2d8a098ded802f2b1261615"
+checksum = "cca49303c05d2a740b8a4552fac63a4db6ead84f7e7eeed04761fd3014c26f25"
 dependencies = [
+ "ahash 0.8.11",
  "aws-credential-types",
  "aws-runtime",
  "aws-sigv4",
@@ -1173,8 +1165,8 @@ dependencies = [
  "aws-smithy-types",
  "aws-smithy-xml",
  "aws-types",
- "bytes 1.8.0",
- "fastrand 2.2.0",
+ "bytes 1.7.2",
+ "fastrand 2.1.1",
  "hex",
  "hmac",
  "http 0.2.12",
@@ -1190,9 +1182,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-sso"
-version = "1.49.0"
+version = "1.40.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09677244a9da92172c8dc60109b4a9658597d4d298b188dd0018b6a66b410ca4"
+checksum = "e5879bec6e74b648ce12f6085e7245417bc5f6d672781028384d2e494be3eb6d"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1203,7 +1195,7 @@ dependencies = [
  "aws-smithy-runtime-api",
  "aws-smithy-types",
  "aws-types",
- "bytes 1.8.0",
+ "bytes 1.7.2",
  "http 0.2.12",
  "once_cell",
  "regex-lite",
@@ -1212,9 +1204,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-ssooidc"
-version = "1.50.0"
+version = "1.41.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81fea2f3a8bb3bd10932ae7ad59cc59f65f270fc9183a7e91f501dc5efbef7ee"
+checksum = "4ef4cd9362f638c22a3b959fd8df292e7e47fdf170270f86246b97109b5f2f7d"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1225,7 +1217,7 @@ dependencies = [
  "aws-smithy-runtime-api",
  "aws-smithy-types",
  "aws-types",
- "bytes 1.8.0",
+ "bytes 1.7.2",
  "http 0.2.12",
  "once_cell",
  "regex-lite",
@@ -1234,9 +1226,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-sts"
-version = "1.50.0"
+version = "1.40.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ada54e5f26ac246dc79727def52f7f8ed38915cb47781e2a72213957dc3a7d5"
+checksum = "0b1e2735d2ab28b35ecbb5496c9d41857f52a0d6a0075bbf6a8af306045ea6f6"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1266,7 +1258,7 @@ dependencies = [
  "aws-smithy-http",
  "aws-smithy-runtime-api",
  "aws-smithy-types",
- "bytes 1.8.0",
+ "bytes 1.7.2",
  "crypto-bigint 0.5.5",
  "form_urlencoded",
  "hex",
@@ -1297,13 +1289,13 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-checksums"
-version = "0.60.13"
+version = "0.60.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba1a71073fca26775c8b5189175ea8863afb1c9ea2cceb02a5de5ad9dfbaa795"
+checksum = "598b1689d001c4d4dc3cb386adb07d37786783aee3ac4b324bcadac116bf3d23"
 dependencies = [
  "aws-smithy-http",
  "aws-smithy-types",
- "bytes 1.8.0",
+ "bytes 1.7.2",
  "crc32c",
  "crc32fast",
  "hex",
@@ -1323,7 +1315,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cef7d0a272725f87e51ba2bf89f8c21e4df61b9e49ae1ac367a6d69916ef7c90"
 dependencies = [
  "aws-smithy-types",
- "bytes 1.8.0",
+ "bytes 1.7.2",
  "crc32fast",
 ]
 
@@ -1336,7 +1328,7 @@ dependencies = [
  "aws-smithy-eventstream",
  "aws-smithy-runtime-api",
  "aws-smithy-types",
- "bytes 1.8.0",
+ "bytes 1.7.2",
  "bytes-utils",
  "futures-core",
  "http 0.2.12",
@@ -1377,8 +1369,8 @@ dependencies = [
  "aws-smithy-http",
  "aws-smithy-runtime-api",
  "aws-smithy-types",
- "bytes 1.8.0",
- "fastrand 2.2.0",
+ "bytes 1.7.2",
+ "fastrand 2.1.1",
  "h2 0.3.26",
  "http 0.2.12",
  "http-body 0.4.6",
@@ -1402,7 +1394,7 @@ checksum = "92165296a47a812b267b4f41032ff8069ab7ff783696d217f0994a0d7ab585cd"
 dependencies = [
  "aws-smithy-async",
  "aws-smithy-types",
- "bytes 1.8.0",
+ "bytes 1.7.2",
  "http 0.2.12",
  "http 1.1.0",
  "pin-project-lite",
@@ -1418,7 +1410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4fbd94a32b3a7d55d3806fe27d98d3ad393050439dd05eb53ece36ec5e3d3510"
 dependencies = [
  "base64-simd",
- "bytes 1.8.0",
+ "bytes 1.7.2",
  "bytes-utils",
  "futures-core",
  "http 0.2.12",
@@ -1439,9 +1431,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-xml"
-version = "0.60.9"
+version = "0.60.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc"
+checksum = "d123fbc2a4adc3c301652ba8e149bf4bc1d1725affb9784eb20c953ace06bf55"
 dependencies = [
  "xmlparser",
 ]
@@ -1470,7 +1462,7 @@ dependencies = [
  "axum-core",
  "base64 0.21.7",
  "bitflags 1.3.2",
- "bytes 1.8.0",
+ "bytes 1.7.2",
  "futures-util",
  "headers",
  "http 0.2.12",
@@ -1503,7 +1495,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
 dependencies = [
  "async-trait",
- "bytes 1.8.0",
+ "bytes 1.7.2",
  "futures-util",
  "http 0.2.12",
  "http-body 0.4.6",
@@ -1520,7 +1512,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f9a320103719de37b7b4da4c8eb629d4573f6bcfd3dfe80d3208806895ccf81d"
 dependencies = [
  "axum",
- "bytes 1.8.0",
+ "bytes 1.7.2",
  "futures-util",
  "http 0.2.12",
  "mime",
@@ -1543,7 +1535,7 @@ dependencies = [
  "addr2line",
  "cfg-if",
  "libc",
- "miniz_oxide",
+ "miniz_oxide 0.8.0",
  "object",
  "rustc-demangle",
  "windows-targets 0.52.6",
@@ -1591,9 +1583,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
 
 [[package]]
 name = "bigdecimal"
-version = "0.4.6"
+version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f850665a0385e070b64c38d2354e6c104c8479c59868d1e48a0c13ee2c7a1c1"
+checksum = "51d712318a27c7150326677b321a5fa91b55f6d9034ffd67f20319e147d40cee"
 dependencies = [
  "autocfg",
  "libm",
@@ -1612,6 +1604,26 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "bindgen"
+version = "0.69.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0"
+dependencies = [
+ "bitflags 2.6.0",
+ "cexpr",
+ "clang-sys",
+ "itertools 0.12.1",
+ "lazy_static",
+ "lazycell",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash 1.1.0",
+ "shlex",
+ "syn 2.0.87",
+]
+
 [[package]]
 name = "bindgen"
 version = "0.70.1"
@@ -1700,9 +1712,9 @@ dependencies = [
 
 [[package]]
 name = "bitstream-io"
-version = "2.6.0"
+version = "2.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2"
+checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452"
 
 [[package]]
 name = "bitvec"
@@ -1777,7 +1789,7 @@ dependencies = [
  "arrayvec",
  "cc",
  "cfg-if",
- "constant_time_eq 0.3.1",
+ "constant_time_eq",
 ]
 
 [[package]]
@@ -1813,15 +1825,15 @@ dependencies = [
  "async-channel 2.3.1",
  "async-task",
  "futures-io",
- "futures-lite 2.5.0",
+ "futures-lite 2.3.0",
  "piper",
 ]
 
 [[package]]
 name = "borsh"
-version = "1.5.3"
+version = "1.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2506947f73ad44e344215ccd6403ac2ae18cd8e046e581a441bf8d199f257f03"
+checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed"
 dependencies = [
  "borsh-derive",
  "cfg_aliases 0.2.1",
@@ -1829,15 +1841,16 @@ dependencies = [
 
 [[package]]
 name = "borsh-derive"
-version = "1.5.3"
+version = "1.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2593a3b8b938bd68373196c9832f516be11fa487ef4ae745eb282e6a56a7244"
+checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b"
 dependencies = [
  "once_cell",
  "proc-macro-crate",
  "proc-macro2",
  "quote",
  "syn 2.0.87",
+ "syn_derive",
 ]
 
 [[package]]
@@ -1855,20 +1868,20 @@ dependencies = [
 
 [[package]]
 name = "bstr"
-version = "1.11.0"
+version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22"
+checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c"
 dependencies = [
  "memchr",
- "regex-automata 0.4.9",
+ "regex-automata 0.4.7",
  "serde",
 ]
 
 [[package]]
 name = "built"
-version = "0.7.5"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b"
+checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4"
 
 [[package]]
 name = "bumpalo"
@@ -1906,18 +1919,18 @@ dependencies = [
 
 [[package]]
 name = "bytemuck"
-version = "1.19.0"
+version = "1.17.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d"
+checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2"
 dependencies = [
  "bytemuck_derive",
 ]
 
 [[package]]
 name = "bytemuck_derive"
-version = "1.8.0"
+version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec"
+checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1948,9 +1961,9 @@ dependencies = [
 
 [[package]]
 name = "bytes"
-version = "1.8.0"
+version = "1.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
+checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
 
 [[package]]
 name = "bytes-utils"
@@ -1958,31 +1971,10 @@ version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35"
 dependencies = [
- "bytes 1.8.0",
+ "bytes 1.7.2",
  "either",
 ]
 
-[[package]]
-name = "bzip2"
-version = "0.4.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
-dependencies = [
- "bzip2-sys",
- "libc",
-]
-
-[[package]]
-name = "bzip2-sys"
-version = "0.1.11+1.0.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
-dependencies = [
- "cc",
- "libc",
- "pkg-config",
-]
-
 [[package]]
 name = "call"
 version = "0.1.0"
@@ -1991,9 +1983,8 @@ dependencies = [
  "audio",
  "client",
  "collections",
- "feature_flags",
  "fs",
- "futures 0.3.31",
+ "futures 0.3.30",
  "gpui",
  "http_client",
  "language",
@@ -2016,10 +2007,10 @@ checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
 dependencies = [
  "bitflags 2.6.0",
  "log",
- "polling 3.7.4",
- "rustix 0.38.40",
+ "polling 3.7.3",
+ "rustix 0.38.35",
  "slab",
- "thiserror 1.0.69",
+ "thiserror",
 ]
 
 [[package]]
@@ -2029,7 +2020,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
 dependencies = [
  "calloop",
- "rustix 0.38.40",
+ "rustix 0.38.35",
  "wayland-backend",
  "wayland-client",
 ]
@@ -2045,9 +2036,9 @@ dependencies = [
 
 [[package]]
 name = "cap-fs-ext"
-version = "3.4.1"
+version = "3.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e16619ada836f12897a72011fe99b03f0025b87a8dbbea4f3c9f89b458a23bf3"
+checksum = "eb23061fc1c4ead4e45ca713080fe768e6234e959f5a5c399c39eb41aa34e56e"
 dependencies = [
  "cap-primitives",
  "cap-std",
@@ -2057,21 +2048,21 @@ dependencies = [
 
 [[package]]
 name = "cap-net-ext"
-version = "3.4.1"
+version = "3.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "710b0eb776410a22c89a98f2f80b2187c2ac3a8206b99f3412332e63c9b09de0"
+checksum = "f83ae11f116bcbafc5327c6af250341db96b5930046732e1905f7dc65887e0e1"
 dependencies = [
  "cap-primitives",
  "cap-std",
- "rustix 0.38.40",
+ "rustix 0.38.35",
  "smallvec",
 ]
 
 [[package]]
 name = "cap-primitives"
-version = "3.4.1"
+version = "3.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82fa6c3f9773feab88d844aa50035a33fb6e7e7426105d2f4bb7aadc42a5f89a"
+checksum = "6d00bd8d26c4270d950eaaa837387964a2089a1c3c349a690a1fa03221d29531"
 dependencies = [
  "ambient-authority",
  "fs-set-times",
@@ -2079,16 +2070,16 @@ dependencies = [
  "io-lifetimes 2.0.3",
  "ipnet",
  "maybe-owned",
- "rustix 0.38.40",
+ "rustix 0.38.35",
  "windows-sys 0.52.0",
  "winx",
 ]
 
 [[package]]
 name = "cap-rand"
-version = "3.4.1"
+version = "3.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53774d49369892b70184f8312e50c1b87edccb376691de4485b0ff554b27c36c"
+checksum = "dbcb16a619d8b8211ed61f42bd290d2a1ac71277a69cf8417ec0996fa92f5211"
 dependencies = [
  "ambient-authority",
  "rand 0.8.5",
@@ -2096,27 +2087,27 @@ dependencies = [
 
 [[package]]
 name = "cap-std"
-version = "3.4.1"
+version = "3.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f71b70818556b4fe2a10c7c30baac3f5f45e973f49fc2673d7c75c39d0baf5b"
+checksum = "19eb8e3d71996828751c1ed3908a439639752ac6bdc874e41469ef7fc15fbd7f"
 dependencies = [
  "cap-primitives",
  "io-extras",
  "io-lifetimes 2.0.3",
- "rustix 0.38.40",
+ "rustix 0.38.35",
 ]
 
 [[package]]
 name = "cap-time-ext"
-version = "3.4.1"
+version = "3.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69dd48afa2363f746c93f961c211f6f099fb594a3446b8097bc5f79db51b6816"
+checksum = "61142dc51e25b7acc970ca578ce2c3695eac22bbba46c1073f5f583e78957725"
 dependencies = [
  "ambient-authority",
  "cap-primitives",
  "iana-time-zone",
  "once_cell",
- "rustix 0.38.40",
+ "rustix 0.38.35",
  "winx",
 ]
 

Cargo.toml 🔗

@@ -363,7 +363,6 @@ heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
 hex = "0.4.3"
 html5ever = "0.27.0"
 hyper = "0.14"
-http = "1.1"
 ignore = "0.4.22"
 image = "0.25.1"
 indexmap = { version = "1.6.2", features = ["serde"] }
@@ -372,7 +371,6 @@ itertools = "0.13.0"
 jsonwebtoken = "9.3"
 libc = "0.2"
 linkify = "0.10.0"
-livekit = { git = "https://github.com/zed-industries/rust-sdks", rev="4262308983646ab5b0e0802c3d8bc52154f99aab", features = ["dispatcher", "services-dispatcher", "rustls-tls-native-roots"], default-features = false }
 log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
 markup5ever_rcdom = "0.3.0"
 nanoid = "0.4"
@@ -551,10 +549,6 @@ features = [
     "Win32_UI_WindowsAndMessaging",
 ]
 
-# TODO livekit https://github.com/RustAudio/cpal/pull/891
-[patch.crates-io]
-cpal = { git = "https://github.com/zed-industries/cpal", rev = "fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50" }
-
 [profile.dev]
 split-debuginfo = "unpacked"
 debug = "limited"

crates/call/Cargo.toml 🔗

@@ -27,7 +27,6 @@ anyhow.workspace = true
 audio.workspace = true
 client.workspace = true
 collections.workspace = true
-feature_flags.workspace = true
 fs.workspace = true
 futures.workspace = true
 gpui.workspace = true

crates/call/src/call.rs 🔗

@@ -18,11 +18,6 @@ use room::Event;
 use settings::Settings;
 use std::sync::Arc;
 
-#[cfg(not(target_os = "windows"))]
-pub use live_kit_client::play_remote_video_track;
-pub use live_kit_client::{
-    track::RemoteVideoTrack, RemoteVideoTrackView, RemoteVideoTrackViewEvent,
-};
 pub use participant::ParticipantLocation;
 pub use room::Room;
 
@@ -31,10 +26,6 @@ struct GlobalActiveCall(Model<ActiveCall>);
 impl Global for GlobalActiveCall {}
 
 pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
-    live_kit_client::init(
-        cx.background_executor().dispatcher.clone(),
-        cx.http_client(),
-    );
     CallSettings::register(cx);
 
     let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx));

crates/call/src/participant.rs 🔗

@@ -1,17 +1,13 @@
-#![cfg_attr(target_os = "windows", allow(unused))]
-
 use anyhow::{anyhow, Result};
-use client::{proto, ParticipantIndex, User};
+use client::ParticipantIndex;
+use client::{proto, User};
 use collections::HashMap;
 use gpui::WeakModel;
-use live_kit_client::AudioStream;
+pub use live_kit_client::Frame;
+pub use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
 use project::Project;
 use std::sync::Arc;
 
-#[cfg(not(target_os = "windows"))]
-pub use live_kit_client::id::TrackSid;
-pub use live_kit_client::track::{RemoteAudioTrack, RemoteVideoTrack};
-
 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
 pub enum ParticipantLocation {
     SharedProject { project_id: u64 },
@@ -43,6 +39,7 @@ pub struct LocalParticipant {
     pub role: proto::ChannelRole,
 }
 
+#[derive(Clone, Debug)]
 pub struct RemoteParticipant {
     pub user: Arc<User>,
     pub peer_id: proto::PeerId,
@@ -52,17 +49,6 @@ pub struct RemoteParticipant {
     pub participant_index: ParticipantIndex,
     pub muted: bool,
     pub speaking: bool,
-    #[cfg(not(target_os = "windows"))]
-    pub video_tracks: HashMap<TrackSid, RemoteVideoTrack>,
-    #[cfg(not(target_os = "windows"))]
-    pub audio_tracks: HashMap<TrackSid, (RemoteAudioTrack, AudioStream)>,
-}
-
-impl RemoteParticipant {
-    pub fn has_video_tracks(&self) -> bool {
-        #[cfg(not(target_os = "windows"))]
-        return !self.video_tracks.is_empty();
-        #[cfg(target_os = "windows")]
-        return false;
-    }
+    pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
+    pub audio_tracks: HashMap<live_kit_client::Sid, Arc<RemoteAudioTrack>>,
 }

crates/call/src/room.rs 🔗

@@ -1,5 +1,3 @@
-#![cfg_attr(target_os = "windows", allow(unused))]
-
 use crate::{
     call_settings::CallSettings,
     participant::{LocalParticipant, ParticipantLocation, RemoteParticipant},
@@ -17,23 +15,11 @@ use gpui::{
     AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
 };
 use language::LanguageRegistry;
-use live_kit_client as livekit;
-#[cfg(not(target_os = "windows"))]
-use livekit::{
-    capture_local_audio_track, capture_local_video_track,
-    id::ParticipantIdentity,
-    options::{TrackPublishOptions, VideoCodec},
-    play_remote_audio_track,
-    publication::LocalTrackPublication,
-    track::{TrackKind, TrackSource},
-    RoomEvent, RoomOptions,
-};
-#[cfg(target_os = "windows")]
-use livekit::{publication::LocalTrackPublication, RoomEvent};
+use live_kit_client::{LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RoomUpdate};
 use postage::{sink::Sink, stream::Stream, watch};
 use project::Project;
 use settings::Settings as _;
-use std::{any::Any, future::Future, mem, sync::Arc, time::Duration};
+use std::{future::Future, mem, sync::Arc, time::Duration};
 use util::{post_inc, ResultExt, TryFutureExt};
 
 pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
@@ -106,10 +92,13 @@ impl Room {
         !self.shared_projects.is_empty()
     }
 
-    #[cfg(all(any(test, feature = "test-support"), not(target_os = "windows")))]
+    #[cfg(any(test, feature = "test-support"))]
     pub fn is_connected(&self) -> bool {
         if let Some(live_kit) = self.live_kit.as_ref() {
-            live_kit.room.connection_state() == livekit::ConnectionState::Connected
+            matches!(
+                *live_kit.room.status().borrow(),
+                live_kit_client::ConnectionState::Connected { .. }
+            )
         } else {
             false
         }
@@ -123,7 +112,77 @@ impl Room {
         user_store: Model<UserStore>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
-        spawn_room_connection(live_kit_connection_info, cx);
+        let live_kit_room = if let Some(connection_info) = live_kit_connection_info {
+            let room = live_kit_client::Room::new();
+            let mut status = room.status();
+            // Consume the initial status of the room.
+            let _ = status.try_recv();
+            let _maintain_room = cx.spawn(|this, mut cx| async move {
+                while let Some(status) = status.next().await {
+                    let this = if let Some(this) = this.upgrade() {
+                        this
+                    } else {
+                        break;
+                    };
+
+                    if status == live_kit_client::ConnectionState::Disconnected {
+                        this.update(&mut cx, |this, cx| this.leave(cx).log_err())
+                            .ok();
+                        break;
+                    }
+                }
+            });
+
+            let _handle_updates = cx.spawn({
+                let room = room.clone();
+                move |this, mut cx| async move {
+                    let mut updates = room.updates();
+                    while let Some(update) = updates.next().await {
+                        let this = if let Some(this) = this.upgrade() {
+                            this
+                        } else {
+                            break;
+                        };
+
+                        this.update(&mut cx, |this, cx| {
+                            this.live_kit_room_updated(update, cx).log_err()
+                        })
+                        .ok();
+                    }
+                }
+            });
+
+            let connect = room.connect(&connection_info.server_url, &connection_info.token);
+            cx.spawn(|this, mut cx| async move {
+                connect.await?;
+                this.update(&mut cx, |this, cx| {
+                    if this.can_use_microphone() {
+                        if let Some(live_kit) = &this.live_kit {
+                            if !live_kit.muted_by_user && !live_kit.deafened {
+                                return this.share_microphone(cx);
+                            }
+                        }
+                    }
+                    Task::ready(Ok(()))
+                })?
+                .await
+            })
+            .detach_and_log_err(cx);
+
+            Some(LiveKitRoom {
+                room,
+                screen_track: LocalTrack::None,
+                microphone_track: LocalTrack::None,
+                next_publish_id: 0,
+                muted_by_user: Self::mute_on_join(cx),
+                deafened: false,
+                speaking: false,
+                _maintain_room,
+                _handle_updates,
+            })
+        } else {
+            None
+        };
 
         let maintain_connection = cx.spawn({
             let client = client.clone();
@@ -137,7 +196,7 @@ impl Room {
         Self {
             id,
             channel_id,
-            live_kit: None,
+            live_kit: live_kit_room,
             status: RoomStatus::Online,
             shared_projects: Default::default(),
             joined_projects: Default::default(),
@@ -647,45 +706,11 @@ impl Room {
         this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))?
     }
 
-    fn apply_room_update(&mut self, room: proto::Room, cx: &mut ModelContext<Self>) -> Result<()> {
-        log::trace!(
-            "client {:?}. room update: {:?}",
-            self.client.user_id(),
-            &room
-        );
-
-        self.pending_room_update = Some(self.start_room_connection(room, cx));
-
-        cx.notify();
-        Ok(())
-    }
-
-    pub fn room_update_completed(&mut self) -> impl Future<Output = ()> {
-        let mut done_rx = self.room_update_completed_rx.clone();
-        async move {
-            while let Some(result) = done_rx.next().await {
-                if result.is_some() {
-                    break;
-                }
-            }
-        }
-    }
-
-    #[cfg(target_os = "windows")]
-    fn start_room_connection(
-        &self,
-        mut room: proto::Room,
-        cx: &mut ModelContext<Self>,
-    ) -> Task<()> {
-        Task::ready(())
-    }
-
-    #[cfg(not(target_os = "windows"))]
-    fn start_room_connection(
-        &self,
+    fn apply_room_update(
+        &mut self,
         mut room: proto::Room,
         cx: &mut ModelContext<Self>,
-    ) -> Task<()> {
+    ) -> Result<()> {
         // Filter ourselves out from the room's participants.
         let local_participant_ix = room
             .participants
@@ -712,7 +737,8 @@ impl Room {
                     user_store.get_users(pending_participant_user_ids, cx),
                 )
             });
-        cx.spawn(|this, mut cx| async move {
+
+        self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
             let (remote_participants, pending_participants) =
                 futures::join!(remote_participants, pending_participants);
 
@@ -750,11 +776,6 @@ impl Room {
                     this.local_participant.projects.clear();
                 }
 
-                let livekit_participants = this
-                    .live_kit
-                    .as_ref()
-                    .map(|live_kit| live_kit.room.remote_participants());
-
                 if let Some(participants) = remote_participants.log_err() {
                     for (participant, user) in room.participants.into_iter().zip(participants) {
                         let Some(peer_id) = participant.peer_id else {
@@ -837,31 +858,40 @@ impl Room {
                                     muted: true,
                                     speaking: false,
                                     video_tracks: Default::default(),
-                                    #[cfg(not(target_os = "windows"))]
                                     audio_tracks: Default::default(),
                                 },
                             );
 
                             Audio::play_sound(Sound::Joined, cx);
-                            if let Some(livekit_participants) = &livekit_participants {
-                                if let Some(livekit_participant) = livekit_participants
-                                    .get(&ParticipantIdentity(user.id.to_string()))
+
+                            if let Some(live_kit) = this.live_kit.as_ref() {
+                                let video_tracks =
+                                    live_kit.room.remote_video_tracks(&user.id.to_string());
+                                let audio_tracks =
+                                    live_kit.room.remote_audio_tracks(&user.id.to_string());
+                                let publications = live_kit
+                                    .room
+                                    .remote_audio_track_publications(&user.id.to_string());
+
+                                for track in video_tracks {
+                                    this.live_kit_room_updated(
+                                        RoomUpdate::SubscribedToRemoteVideoTrack(track),
+                                        cx,
+                                    )
+                                    .log_err();
+                                }
+
+                                for (track, publication) in
+                                    audio_tracks.iter().zip(publications.iter())
                                 {
-                                    for publication in
-                                        livekit_participant.track_publications().into_values()
-                                    {
-                                        if let Some(track) = publication.track() {
-                                            this.live_kit_room_updated(
-                                                RoomEvent::TrackSubscribed {
-                                                    track,
-                                                    publication,
-                                                    participant: livekit_participant.clone(),
-                                                },
-                                                cx,
-                                            )
-                                            .warn_on_err();
-                                        }
-                                    }
+                                    this.live_kit_room_updated(
+                                        RoomUpdate::SubscribedToRemoteAudioTrack(
+                                            track.clone(),
+                                            publication.clone(),
+                                        ),
+                                        cx,
+                                    )
+                                    .log_err();
                                 }
                             }
                         }
@@ -929,89 +959,61 @@ impl Room {
                 cx.notify();
             })
             .ok();
-        })
+        }));
+
+        cx.notify();
+        Ok(())
+    }
+
+    pub fn room_update_completed(&mut self) -> impl Future<Output = ()> {
+        let mut done_rx = self.room_update_completed_rx.clone();
+        async move {
+            while let Some(result) = done_rx.next().await {
+                if result.is_some() {
+                    break;
+                }
+            }
+        }
     }
 
     fn live_kit_room_updated(
         &mut self,
-        event: RoomEvent,
+        update: RoomUpdate,
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
-        log::trace!(
-            "client {:?}. livekit event: {:?}",
-            self.client.user_id(),
-            &event
-        );
-
-        match event {
-            #[cfg(not(target_os = "windows"))]
-            RoomEvent::TrackSubscribed {
-                track,
-                participant,
-                publication,
-            } => {
-                let user_id = participant.identity().0.parse()?;
-                let track_id = track.sid();
-                let participant = self.remote_participants.get_mut(&user_id).ok_or_else(|| {
-                    anyhow!(
-                        "{:?} subscribed to track by unknown participant {user_id}",
-                        self.client.user_id()
-                    )
-                })?;
-                if self.live_kit.as_ref().map_or(true, |kit| kit.deafened) {
-                    track.rtc_track().set_enabled(false);
-                }
-                match track {
-                    livekit::track::RemoteTrack::Audio(track) => {
-                        cx.emit(Event::RemoteAudioTracksChanged {
-                            participant_id: participant.peer_id,
-                        });
-                        let stream = play_remote_audio_track(&track, cx);
-                        participant.audio_tracks.insert(track_id, (track, stream));
-                        participant.muted = publication.is_muted();
-                    }
-                    livekit::track::RemoteTrack::Video(track) => {
-                        cx.emit(Event::RemoteVideoTracksChanged {
-                            participant_id: participant.peer_id,
-                        });
-                        participant.video_tracks.insert(track_id, track);
-                    }
-                }
+        match update {
+            RoomUpdate::SubscribedToRemoteVideoTrack(track) => {
+                let user_id = track.publisher_id().parse()?;
+                let track_id = track.sid().to_string();
+                let participant = self
+                    .remote_participants
+                    .get_mut(&user_id)
+                    .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
+                participant.video_tracks.insert(track_id.clone(), track);
+                cx.emit(Event::RemoteVideoTracksChanged {
+                    participant_id: participant.peer_id,
+                });
             }
 
-            #[cfg(not(target_os = "windows"))]
-            RoomEvent::TrackUnsubscribed {
-                track, participant, ..
+            RoomUpdate::UnsubscribedFromRemoteVideoTrack {
+                publisher_id,
+                track_id,
             } => {
-                let user_id = participant.identity().0.parse()?;
-                let participant = self.remote_participants.get_mut(&user_id).ok_or_else(|| {
-                    anyhow!(
-                        "{:?}, unsubscribed from track by unknown participant {user_id}",
-                        self.client.user_id()
-                    )
-                })?;
-                match track {
-                    livekit::track::RemoteTrack::Audio(track) => {
-                        participant.audio_tracks.remove(&track.sid());
-                        participant.muted = true;
-                        cx.emit(Event::RemoteAudioTracksChanged {
-                            participant_id: participant.peer_id,
-                        });
-                    }
-                    livekit::track::RemoteTrack::Video(track) => {
-                        participant.video_tracks.remove(&track.sid());
-                        cx.emit(Event::RemoteVideoTracksChanged {
-                            participant_id: participant.peer_id,
-                        });
-                    }
-                }
+                let user_id = publisher_id.parse()?;
+                let participant = self
+                    .remote_participants
+                    .get_mut(&user_id)
+                    .ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
+                participant.video_tracks.remove(&track_id);
+                cx.emit(Event::RemoteVideoTracksChanged {
+                    participant_id: participant.peer_id,
+                });
             }
 
-            #[cfg(not(target_os = "windows"))]
-            RoomEvent::ActiveSpeakersChanged { speakers } => {
+            RoomUpdate::ActiveSpeakersChanged { speakers } => {
                 let mut speaker_ids = speakers
                     .into_iter()
-                    .filter_map(|speaker| speaker.identity().0.parse().ok())
+                    .filter_map(|speaker_sid| speaker_sid.parse().ok())
                     .collect::<Vec<u64>>();
                 speaker_ids.sort_unstable();
                 for (sid, participant) in &mut self.remote_participants {
@@ -1024,65 +1026,82 @@ impl Room {
                 }
             }
 
-            #[cfg(not(target_os = "windows"))]
-            RoomEvent::TrackMuted {
-                participant,
-                publication,
-            }
-            | RoomEvent::TrackUnmuted {
-                participant,
-                publication,
-            } => {
+            RoomUpdate::RemoteAudioTrackMuteChanged { track_id, muted } => {
                 let mut found = false;
-                let user_id = participant.identity().0.parse()?;
-                let track_id = publication.sid();
-                if let Some(participant) = self.remote_participants.get_mut(&user_id) {
-                    for (track, _) in participant.audio_tracks.values() {
+                for participant in &mut self.remote_participants.values_mut() {
+                    for track in participant.audio_tracks.values() {
                         if track.sid() == track_id {
                             found = true;
                             break;
                         }
                     }
                     if found {
-                        participant.muted = publication.is_muted();
+                        participant.muted = muted;
+                        break;
                     }
                 }
             }
 
-            #[cfg(not(target_os = "windows"))]
-            RoomEvent::LocalTrackUnpublished { publication, .. } => {
-                log::info!("unpublished track {}", publication.sid());
-                if let Some(room) = &mut self.live_kit {
-                    if let LocalTrack::Published {
-                        track_publication, ..
-                    } = &room.microphone_track
-                    {
-                        if track_publication.sid() == publication.sid() {
-                            room.microphone_track = LocalTrack::None;
-                        }
-                    }
-                    if let LocalTrack::Published {
-                        track_publication, ..
-                    } = &room.screen_track
-                    {
-                        if track_publication.sid() == publication.sid() {
-                            room.screen_track = LocalTrack::None;
-                        }
+            RoomUpdate::SubscribedToRemoteAudioTrack(track, publication) => {
+                if let Some(live_kit) = &self.live_kit {
+                    if live_kit.deafened {
+                        track.stop();
+                        cx.foreground_executor()
+                            .spawn(publication.set_enabled(false))
+                            .detach();
                     }
                 }
+
+                let user_id = track.publisher_id().parse()?;
+                let track_id = track.sid().to_string();
+                let participant = self
+                    .remote_participants
+                    .get_mut(&user_id)
+                    .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
+                participant.audio_tracks.insert(track_id.clone(), track);
+                participant.muted = publication.is_muted();
+
+                cx.emit(Event::RemoteAudioTracksChanged {
+                    participant_id: participant.peer_id,
+                });
             }
 
-            #[cfg(not(target_os = "windows"))]
-            RoomEvent::LocalTrackPublished { publication, .. } => {
-                log::info!("published track {:?}", publication.sid());
+            RoomUpdate::UnsubscribedFromRemoteAudioTrack {
+                publisher_id,
+                track_id,
+            } => {
+                let user_id = publisher_id.parse()?;
+                let participant = self
+                    .remote_participants
+                    .get_mut(&user_id)
+                    .ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
+                participant.audio_tracks.remove(&track_id);
+                cx.emit(Event::RemoteAudioTracksChanged {
+                    participant_id: participant.peer_id,
+                });
             }
 
-            #[cfg(not(target_os = "windows"))]
-            RoomEvent::Disconnected { reason } => {
-                log::info!("disconnected from room: {reason:?}");
-                self.leave(cx).detach_and_log_err(cx);
+            RoomUpdate::LocalAudioTrackUnpublished { publication } => {
+                log::info!("unpublished audio track {}", publication.sid());
+                if let Some(room) = &mut self.live_kit {
+                    room.microphone_track = LocalTrack::None;
+                }
+            }
+
+            RoomUpdate::LocalVideoTrackUnpublished { publication } => {
+                log::info!("unpublished video track {}", publication.sid());
+                if let Some(room) = &mut self.live_kit {
+                    room.screen_track = LocalTrack::None;
+                }
+            }
+
+            RoomUpdate::LocalAudioTrackPublished { publication } => {
+                log::info!("published audio track {}", publication.sid());
+            }
+
+            RoomUpdate::LocalVideoTrackPublished { publication } => {
+                log::info!("published video track {}", publication.sid());
             }
-            _ => {}
         }
 
         cx.notify();
@@ -1298,17 +1317,8 @@ impl Room {
         self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
     }
 
-    pub fn can_use_microphone(&self, _cx: &AppContext) -> bool {
+    pub fn can_use_microphone(&self) -> bool {
         use proto::ChannelRole::*;
-
-        #[cfg(not(any(test, feature = "test-support")))]
-        {
-            use feature_flags::FeatureFlagAppExt as _;
-            if cfg!(target_os = "windows") || (cfg!(target_os = "linux") && !_cx.is_staff()) {
-                return false;
-            }
-        }
-
         match self.local_participant.role {
             Admin | Member | Talker => true,
             Guest | Banned => false,
@@ -1323,177 +1333,161 @@ impl Room {
         }
     }
 
-    #[cfg(target_os = "windows")]
-    pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
-        Task::ready(Err(anyhow!("Windows is not supported yet")))
-    }
-
-    #[cfg(not(target_os = "windows"))]
     #[track_caller]
     pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
         if self.status.is_offline() {
             return Task::ready(Err(anyhow!("room is offline")));
         }
 
-        let (participant, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
+        let publish_id = if let Some(live_kit) = self.live_kit.as_mut() {
             let publish_id = post_inc(&mut live_kit.next_publish_id);
             live_kit.microphone_track = LocalTrack::Pending { publish_id };
             cx.notify();
-            (live_kit.room.local_participant(), publish_id)
+            publish_id
         } else {
             return Task::ready(Err(anyhow!("live-kit was not initialized")));
         };
 
         cx.spawn(move |this, mut cx| async move {
-            let (track, stream) = cx.update(capture_local_audio_track)??;
-
-            let publication = participant
-                .publish_track(
-                    livekit::track::LocalTrack::Audio(track),
-                    TrackPublishOptions {
-                        source: TrackSource::Microphone,
-                        ..Default::default()
-                    },
-                )
-                .await
-                .map_err(|error| anyhow!("failed to publish track: {error}"));
-            this.update(&mut cx, |this, cx| {
-                let live_kit = this
-                    .live_kit
-                    .as_mut()
-                    .ok_or_else(|| anyhow!("live-kit was not initialized"))?;
-
-                let canceled = if let LocalTrack::Pending {
-                    publish_id: cur_publish_id,
-                } = &live_kit.microphone_track
-                {
-                    *cur_publish_id != publish_id
-                } else {
-                    true
-                };
-
-                match publication {
-                    Ok(publication) => {
-                        if canceled {
-                            cx.background_executor()
-                                .spawn(async move {
-                                    participant.unpublish_track(&publication.sid()).await
-                                })
-                                .detach_and_log_err(cx)
-                        } else {
-                            if live_kit.muted_by_user || live_kit.deafened {
-                                publication.mute();
+            let publish_track = async {
+                let track = LocalAudioTrack::create();
+                this.upgrade()
+                    .ok_or_else(|| anyhow!("room was dropped"))?
+                    .update(&mut cx, |this, _| {
+                        this.live_kit
+                            .as_ref()
+                            .map(|live_kit| live_kit.room.publish_audio_track(track))
+                    })?
+                    .ok_or_else(|| anyhow!("live-kit was not initialized"))?
+                    .await
+            };
+            let publication = publish_track.await;
+            this.upgrade()
+                .ok_or_else(|| anyhow!("room was dropped"))?
+                .update(&mut cx, |this, cx| {
+                    let live_kit = this
+                        .live_kit
+                        .as_mut()
+                        .ok_or_else(|| anyhow!("live-kit was not initialized"))?;
+
+                    let canceled = if let LocalTrack::Pending {
+                        publish_id: cur_publish_id,
+                    } = &live_kit.microphone_track
+                    {
+                        *cur_publish_id != publish_id
+                    } else {
+                        true
+                    };
+
+                    match publication {
+                        Ok(publication) => {
+                            if canceled {
+                                live_kit.room.unpublish_track(publication);
+                            } else {
+                                if live_kit.muted_by_user || live_kit.deafened {
+                                    cx.background_executor()
+                                        .spawn(publication.set_mute(true))
+                                        .detach();
+                                }
+                                live_kit.microphone_track = LocalTrack::Published {
+                                    track_publication: publication,
+                                };
+                                cx.notify();
                             }
-                            live_kit.microphone_track = LocalTrack::Published {
-                                track_publication: publication,
-                                _stream: Box::new(stream),
-                            };
-                            cx.notify();
-                        }
-                        Ok(())
-                    }
-                    Err(error) => {
-                        if canceled {
                             Ok(())
-                        } else {
-                            live_kit.microphone_track = LocalTrack::None;
-                            cx.notify();
-                            Err(error)
+                        }
+                        Err(error) => {
+                            if canceled {
+                                Ok(())
+                            } else {
+                                live_kit.microphone_track = LocalTrack::None;
+                                cx.notify();
+                                Err(error)
+                            }
                         }
                     }
-                }
-            })?
+                })?
         })
     }
 
-    #[cfg(target_os = "windows")]
-    pub fn share_screen(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
-        Task::ready(Err(anyhow!("Windows is not supported yet")))
-    }
-
-    #[cfg(not(target_os = "windows"))]
     pub fn share_screen(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
         if self.status.is_offline() {
             return Task::ready(Err(anyhow!("room is offline")));
-        }
-        if self.is_screen_sharing() {
+        } else if self.is_screen_sharing() {
             return Task::ready(Err(anyhow!("screen was already shared")));
         }
 
-        let (participant, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
+        let (displays, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
             let publish_id = post_inc(&mut live_kit.next_publish_id);
             live_kit.screen_track = LocalTrack::Pending { publish_id };
             cx.notify();
-            (live_kit.room.local_participant(), publish_id)
+            (live_kit.room.display_sources(), publish_id)
         } else {
             return Task::ready(Err(anyhow!("live-kit was not initialized")));
         };
 
-        let sources = cx.screen_capture_sources();
-
         cx.spawn(move |this, mut cx| async move {
-            let sources = sources.await??;
-            let source = sources.first().ok_or_else(|| anyhow!("no display found"))?;
-
-            let (track, stream) = capture_local_video_track(&**source).await?;
-
-            let publication = participant
-                .publish_track(
-                    livekit::track::LocalTrack::Video(track),
-                    TrackPublishOptions {
-                        source: TrackSource::Screenshare,
-                        video_codec: VideoCodec::H264,
-                        ..Default::default()
-                    },
-                )
-                .await
-                .map_err(|error| anyhow!("error publishing screen track {error:?}"));
+            let publish_track = async {
+                let displays = displays.await?;
+                let display = displays
+                    .first()
+                    .ok_or_else(|| anyhow!("no display found"))?;
+                let track = LocalVideoTrack::screen_share_for_display(display);
+                this.upgrade()
+                    .ok_or_else(|| anyhow!("room was dropped"))?
+                    .update(&mut cx, |this, _| {
+                        this.live_kit
+                            .as_ref()
+                            .map(|live_kit| live_kit.room.publish_video_track(track))
+                    })?
+                    .ok_or_else(|| anyhow!("live-kit was not initialized"))?
+                    .await
+            };
 
-            this.update(&mut cx, |this, cx| {
-                let live_kit = this
-                    .live_kit
-                    .as_mut()
-                    .ok_or_else(|| anyhow!("live-kit was not initialized"))?;
-
-                let canceled = if let LocalTrack::Pending {
-                    publish_id: cur_publish_id,
-                } = &live_kit.screen_track
-                {
-                    *cur_publish_id != publish_id
-                } else {
-                    true
-                };
-
-                match publication {
-                    Ok(publication) => {
-                        if canceled {
-                            cx.background_executor()
-                                .spawn(async move {
-                                    participant.unpublish_track(&publication.sid()).await
-                                })
-                                .detach()
-                        } else {
-                            live_kit.screen_track = LocalTrack::Published {
-                                track_publication: publication,
-                                _stream: Box::new(stream),
-                            };
-                            cx.notify();
-                        }
+            let publication = publish_track.await;
+            this.upgrade()
+                .ok_or_else(|| anyhow!("room was dropped"))?
+                .update(&mut cx, |this, cx| {
+                    let live_kit = this
+                        .live_kit
+                        .as_mut()
+                        .ok_or_else(|| anyhow!("live-kit was not initialized"))?;
+
+                    let canceled = if let LocalTrack::Pending {
+                        publish_id: cur_publish_id,
+                    } = &live_kit.screen_track
+                    {
+                        *cur_publish_id != publish_id
+                    } else {
+                        true
+                    };
+
+                    match publication {
+                        Ok(publication) => {
+                            if canceled {
+                                live_kit.room.unpublish_track(publication);
+                            } else {
+                                live_kit.screen_track = LocalTrack::Published {
+                                    track_publication: publication,
+                                };
+                                cx.notify();
+                            }
+
+                            Audio::play_sound(Sound::StartScreenshare, cx);
 
-                        Audio::play_sound(Sound::StartScreenshare, cx);
-                        Ok(())
-                    }
-                    Err(error) => {
-                        if canceled {
                             Ok(())
-                        } else {
-                            live_kit.screen_track = LocalTrack::None;
-                            cx.notify();
-                            Err(error)
+                        }
+                        Err(error) => {
+                            if canceled {
+                                Ok(())
+                            } else {
+                                live_kit.screen_track = LocalTrack::None;
+                                cx.notify();
+                                Err(error)
+                            }
                         }
                     }
-                }
-            })?
+                })?
         })
     }
 
@@ -1518,7 +1512,9 @@ impl Room {
             }
 
             if should_undeafen {
-                self.set_deafened(false, cx);
+                if let Some(task) = self.set_deafened(false, cx) {
+                    task.detach_and_log_err(cx);
+                }
             }
         }
     }
@@ -1531,7 +1527,9 @@ impl Room {
             live_kit.deafened = deafened;
             let should_change_mute = !live_kit.muted_by_user;
 
-            self.set_deafened(deafened, cx);
+            if let Some(task) = self.set_deafened(deafened, cx) {
+                task.detach_and_log_err(cx);
+            }
 
             if should_change_mute {
                 if let Some(task) = self.set_mute(deafened, cx) {
@@ -1559,36 +1557,47 @@ impl Room {
             LocalTrack::Published {
                 track_publication, ..
             } => {
-                #[cfg(not(target_os = "windows"))]
-                {
-                    let local_participant = live_kit.room.local_participant();
-                    let sid = track_publication.sid();
-                    cx.background_executor()
-                        .spawn(async move { local_participant.unpublish_track(&sid).await })
-                        .detach_and_log_err(cx);
-                    cx.notify();
-                }
+                live_kit.room.unpublish_track(track_publication);
+                cx.notify();
+
                 Audio::play_sound(Sound::StopScreenshare, cx);
                 Ok(())
             }
         }
     }
 
-    fn set_deafened(&mut self, deafened: bool, cx: &mut ModelContext<Self>) -> Option<()> {
-        #[cfg(not(target_os = "windows"))]
-        {
-            let live_kit = self.live_kit.as_mut()?;
-            cx.notify();
-            for (_, participant) in live_kit.room.remote_participants() {
-                for (_, publication) in participant.track_publications() {
-                    if publication.kind() == TrackKind::Audio {
-                        publication.set_enabled(!deafened);
-                    }
+    fn set_deafened(
+        &mut self,
+        deafened: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let live_kit = self.live_kit.as_mut()?;
+        cx.notify();
+
+        let mut track_updates = Vec::new();
+        for participant in self.remote_participants.values() {
+            for publication in live_kit
+                .room
+                .remote_audio_track_publications(&participant.user.id.to_string())
+            {
+                track_updates.push(publication.set_enabled(!deafened));
+            }
+
+            for track in participant.audio_tracks.values() {
+                if deafened {
+                    track.stop();
+                } else {
+                    track.start();
                 }
             }
         }
 
-        None
+        Some(cx.foreground_executor().spawn(async move {
+            for result in futures::future::join_all(track_updates).await {
+                result?;
+            }
+            Ok(())
+        }))
     }
 
     fn set_mute(
@@ -1614,84 +1623,25 @@ impl Room {
                 }
             }
             LocalTrack::Pending { .. } => None,
-            LocalTrack::Published {
-                track_publication, ..
-            } => {
-                #[cfg(not(target_os = "windows"))]
-                {
-                    if should_mute {
-                        track_publication.mute()
-                    } else {
-                        track_publication.unmute()
-                    }
-                }
-                None
-            }
+            LocalTrack::Published { track_publication } => Some(
+                cx.foreground_executor()
+                    .spawn(track_publication.set_mute(should_mute)),
+            ),
         }
     }
-}
-
-#[cfg(target_os = "windows")]
-fn spawn_room_connection(
-    live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
-    cx: &mut ModelContext<'_, Room>,
-) {
-}
-
-#[cfg(not(target_os = "windows"))]
-fn spawn_room_connection(
-    live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
-    cx: &mut ModelContext<'_, Room>,
-) {
-    if let Some(connection_info) = live_kit_connection_info {
-        cx.spawn(|this, mut cx| async move {
-            let (room, mut events) = livekit::Room::connect(
-                &connection_info.server_url,
-                &connection_info.token,
-                RoomOptions::default(),
-            )
-            .await?;
-
-            this.update(&mut cx, |this, cx| {
-                let _handle_updates = cx.spawn(|this, mut cx| async move {
-                    while let Some(event) = events.recv().await {
-                        if this
-                            .update(&mut cx, |this, cx| {
-                                this.live_kit_room_updated(event, cx).warn_on_err();
-                            })
-                            .is_err()
-                        {
-                            break;
-                        }
-                    }
-                });
 
-                let muted_by_user = Room::mute_on_join(cx);
-                this.live_kit = Some(LiveKitRoom {
-                    room: Arc::new(room),
-                    screen_track: LocalTrack::None,
-                    microphone_track: LocalTrack::None,
-                    next_publish_id: 0,
-                    muted_by_user,
-                    deafened: false,
-                    speaking: false,
-                    _handle_updates,
-                });
-
-                if !muted_by_user && this.can_use_microphone(cx) {
-                    this.share_microphone(cx)
-                } else {
-                    Task::ready(Ok(()))
-                }
-            })?
-            .await
-        })
-        .detach_and_log_err(cx);
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn set_display_sources(&self, sources: Vec<live_kit_client::MacOSDisplay>) {
+        self.live_kit
+            .as_ref()
+            .unwrap()
+            .room
+            .set_display_sources(sources);
     }
 }
 
 struct LiveKitRoom {
-    room: Arc<livekit::Room>,
+    room: Arc<live_kit_client::Room>,
     screen_track: LocalTrack,
     microphone_track: LocalTrack,
     /// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user.

crates/collab/src/tests.rs 🔗

@@ -1,6 +1,3 @@
-// todo(windows): Actually run the tests
-#![cfg(not(target_os = "windows"))]
-
 use std::sync::Arc;
 
 use call::Room;

crates/collab/src/tests/channel_guest_tests.rs 🔗

@@ -107,9 +107,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
     });
     assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
     assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
-    cx_b.update(|cx_b| {
-        assert!(room_b.read_with(cx_b, |room, cx| !room.can_use_microphone(cx)));
-    });
+    assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
     assert!(room_b
         .update(cx_b, |room, cx| room.share_microphone(cx))
         .await
@@ -135,9 +133,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
     assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
 
     // B sees themselves as muted, and can unmute.
-    cx_b.update(|cx_b| {
-        assert!(room_b.read_with(cx_b, |room, cx| room.can_use_microphone(cx)));
-    });
+    assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
     room_b.read_with(cx_b, |room, _| assert!(room.is_muted()));
     room_b.update(cx_b, |room, cx| room.toggle_mute(cx));
     cx_a.run_until_parked();
@@ -230,9 +226,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
     let room_b = cx_b
         .read(ActiveCall::global)
         .update(cx_b, |call, _| call.room().unwrap().clone());
-    cx_b.update(|cx_b| {
-        assert!(room_b.read_with(cx_b, |room, cx| !room.can_use_microphone(cx)));
-    });
+    assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
 
     // A tries to grant write access to B, but cannot because B has not
     // yet signed the zed CLA.
@@ -250,9 +244,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
         .unwrap_err();
     cx_a.run_until_parked();
     assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
-    cx_b.update(|cx_b| {
-        assert!(room_b.read_with(cx_b, |room, cx| !room.can_use_microphone(cx)));
-    });
+    assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
 
     // A tries to grant write access to B, but cannot because B has not
     // yet signed the zed CLA.
@@ -270,9 +262,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
         .unwrap();
     cx_a.run_until_parked();
     assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
-    cx_b.update(|cx_b| {
-        assert!(room_b.read_with(cx_b, |room, cx| room.can_use_microphone(cx)));
-    });
+    assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
 
     // User B signs the zed CLA.
     server
@@ -297,7 +287,5 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
         .unwrap();
     cx_a.run_until_parked();
     assert!(room_b.read_with(cx_b, |room, _| room.can_share_projects()));
-    cx_b.update(|cx_b| {
-        assert!(room_b.read_with(cx_b, |room, cx| room.can_use_microphone(cx)));
-    });
+    assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
 }

crates/collab/src/tests/following_tests.rs 🔗

@@ -9,9 +9,10 @@ use collab_ui::{
 use editor::{Editor, ExcerptRange, MultiBuffer};
 use gpui::{
     point, BackgroundExecutor, BorrowAppContext, Context, Entity, SharedString, TestAppContext,
-    TestScreenCaptureSource, View, VisualContext, VisualTestContext,
+    View, VisualContext, VisualTestContext,
 };
 use language::Capability;
+use live_kit_client::MacOSDisplay;
 use project::WorktreeSettings;
 use rpc::proto::PeerId;
 use serde_json::json;
@@ -428,17 +429,17 @@ async fn test_basic_following(
     );
 
     // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
-    let display = TestScreenCaptureSource::new();
+    let display = MacOSDisplay::new();
     active_call_b
         .update(cx_b, |call, cx| call.set_location(None, cx))
         .await
         .unwrap();
-    cx_b.set_screen_capture_sources(vec![display]);
     active_call_b
         .update(cx_b, |call, cx| {
-            call.room()
-                .unwrap()
-                .update(cx, |room, cx| room.share_screen(cx))
+            call.room().unwrap().update(cx, |room, cx| {
+                room.set_display_sources(vec![display.clone()]);
+                room.share_screen(cx)
+            })
         })
         .await
         .unwrap();

crates/collab/src/tests/integration_tests.rs 🔗

@@ -15,7 +15,7 @@ use futures::{channel::mpsc, StreamExt as _};
 use git::repository::GitFileStatus;
 use gpui::{
     px, size, AppContext, BackgroundExecutor, Model, Modifiers, MouseButton, MouseDownEvent,
-    TestAppContext, TestScreenCaptureSource, UpdateGlobal,
+    TestAppContext, UpdateGlobal,
 };
 use language::{
     language_settings::{
@@ -24,6 +24,7 @@ use language::{
     tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticEntry, FakeLspAdapter,
     Language, LanguageConfig, LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
 };
+use live_kit_client::MacOSDisplay;
 use lsp::LanguageServerId;
 use parking_lot::Mutex;
 use project::lsp_store::FormatTarget;
@@ -240,15 +241,15 @@ async fn test_basic_calls(
     );
 
     // User A shares their screen
-    let display = TestScreenCaptureSource::new();
+    let display = MacOSDisplay::new();
     let events_b = active_call_events(cx_b);
     let events_c = active_call_events(cx_c);
-    cx_a.set_screen_capture_sources(vec![display]);
     active_call_a
         .update(cx_a, |call, cx| {
-            call.room()
-                .unwrap()
-                .update(cx, |room, cx| room.share_screen(cx))
+            call.room().unwrap().update(cx, |room, cx| {
+                room.set_display_sources(vec![display.clone()]);
+                room.share_screen(cx)
+            })
         })
         .await
         .unwrap();
@@ -1941,7 +1942,7 @@ async fn test_mute_deafen(
     room_a.read_with(cx_a, |room, _| assert!(!room.is_muted()));
     room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
 
-    // Users A and B are both unmuted.
+    // Users A and B are both muted.
     assert_eq!(
         participant_audio_state(&room_a, cx_a),
         &[ParticipantAudioState {
@@ -2073,7 +2074,7 @@ async fn test_mute_deafen(
                     audio_tracks_playing: participant
                         .audio_tracks
                         .values()
-                        .map(|(track, _)| track.rtc_track().enabled())
+                        .map(|track| track.is_playing())
                         .collect(),
                 })
                 .collect::<Vec<_>>()
@@ -6056,13 +6057,13 @@ async fn test_join_call_after_screen_was_shared(
     assert_eq!(call_b.calling_user.github_login, "user_a");
 
     // User A shares their screen
-    let display = TestScreenCaptureSource::new();
-    cx_a.set_screen_capture_sources(vec![display]);
+    let display = MacOSDisplay::new();
     active_call_a
         .update(cx_a, |call, cx| {
-            call.room()
-                .unwrap()
-                .update(cx, |room, cx| room.share_screen(cx))
+            call.room().unwrap().update(cx, |room, cx| {
+                room.set_display_sources(vec![display.clone()]);
+                room.share_screen(cx)
+            })
         })
         .await
         .unwrap();

crates/collab/src/tests/test_server.rs 🔗

@@ -47,7 +47,7 @@ use workspace::{Workspace, WorkspaceStore};
 
 pub struct TestServer {
     pub app_state: Arc<AppState>,
-    pub test_live_kit_server: Arc<live_kit_client::test::TestServer>,
+    pub test_live_kit_server: Arc<live_kit_client::TestServer>,
     server: Arc<Server>,
     next_github_user_id: i32,
     connection_killers: Arc<Mutex<HashMap<PeerId, Arc<AtomicBool>>>>,
@@ -89,7 +89,7 @@ impl TestServer {
             TestDb::sqlite(deterministic.clone())
         };
         let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst);
-        let live_kit_server = live_kit_client::test::TestServer::create(
+        let live_kit_server = live_kit_client::TestServer::create(
             format!("http://livekit.{}.test", live_kit_server_id),
             format!("devkey-{}", live_kit_server_id),
             format!("secret-{}", live_kit_server_id),
@@ -499,7 +499,7 @@ impl TestServer {
 
     pub async fn build_app_state(
         test_db: &TestDb,
-        live_kit_test_server: &live_kit_client::test::TestServer,
+        live_kit_test_server: &live_kit_client::TestServer,
         executor: Executor,
     ) -> Arc<AppState> {
         Arc::new(AppState {

crates/collab_ui/src/collab_panel.rs 🔗

@@ -474,10 +474,11 @@ impl CollabPanel {
                             project_id: project.id,
                             worktree_root_names: project.worktree_root_names.clone(),
                             host_user_id: participant.user.id,
-                            is_last: projects.peek().is_none() && !participant.has_video_tracks(),
+                            is_last: projects.peek().is_none()
+                                && participant.video_tracks.is_empty(),
                         });
                     }
-                    if participant.has_video_tracks() {
+                    if !participant.video_tracks.is_empty() {
                         self.entries.push(ListEntry::ParticipantScreen {
                             peer_id: Some(participant.peer_id),
                             is_last: true,

crates/gpui/build.rs 🔗

@@ -48,7 +48,6 @@ mod macos {
 
     fn generate_dispatch_bindings() {
         println!("cargo:rustc-link-lib=framework=System");
-        println!("cargo:rustc-link-lib=framework=ScreenCaptureKit");
         println!("cargo:rerun-if-changed=src/platform/mac/dispatch.h");
 
         let bindings = bindgen::Builder::default()

crates/gpui/src/app.rs 🔗

@@ -33,8 +33,8 @@ use crate::{
     Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap, Keystroke, LayoutId,
     Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point,
     PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation,
-    ScreenCaptureSource, SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem,
-    View, ViewContext, Window, WindowAppearance, WindowContext, WindowHandle, WindowId,
+    SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, View, ViewContext,
+    Window, WindowAppearance, WindowContext, WindowHandle, WindowId,
 };
 
 mod async_context;
@@ -599,13 +599,6 @@ impl AppContext {
         self.platform.primary_display()
     }
 
-    /// Returns a list of available screen capture sources.
-    pub fn screen_capture_sources(
-        &self,
-    ) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
-        self.platform.screen_capture_sources()
-    }
-
     /// Returns the display with the given ID, if one exists.
     pub fn find_display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
         self.displays()

crates/gpui/src/app/test_context.rs 🔗

@@ -4,8 +4,8 @@ use crate::{
     Element, Empty, Entity, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Model,
     ModelContext, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
     MouseUpEvent, Pixels, Platform, Point, Render, Result, Size, Task, TestDispatcher,
-    TestPlatform, TestScreenCaptureSource, TestWindow, TextSystem, View, ViewContext,
-    VisualContext, WindowBounds, WindowContext, WindowHandle, WindowOptions,
+    TestPlatform, TestWindow, TextSystem, View, ViewContext, VisualContext, WindowBounds,
+    WindowContext, WindowHandle, WindowOptions,
 };
 use anyhow::{anyhow, bail};
 use futures::{channel::oneshot, Stream, StreamExt};
@@ -287,12 +287,6 @@ impl TestAppContext {
         self.test_window(window_handle).simulate_resize(size);
     }
 
-    /// Causes the given sources to be returned if the application queries for screen
-    /// capture sources.
-    pub fn set_screen_capture_sources(&self, sources: Vec<TestScreenCaptureSource>) {
-        self.test_platform.set_screen_capture_sources(sources);
-    }
-
     /// Returns all windows open in the test.
     pub fn windows(&self) -> Vec<AnyWindowHandle> {
         self.app.borrow().windows().clone()

crates/gpui/src/geometry.rs 🔗

@@ -704,11 +704,6 @@ pub struct Bounds<T: Clone + Default + Debug> {
     pub size: Size<T>,
 }
 
-/// Create a bounds with the given origin and size
-pub fn bounds<T: Clone + Default + Debug>(origin: Point<T>, size: Size<T>) -> Bounds<T> {
-    Bounds { origin, size }
-}
-
 impl Bounds<Pixels> {
     /// Generate a centered bounds for the given display or primary display if none is provided
     pub fn centered(display_id: Option<DisplayId>, size: Size<Pixels>, cx: &AppContext) -> Self {

crates/gpui/src/platform.rs 🔗

@@ -70,9 +70,6 @@ pub(crate) use test::*;
 #[cfg(target_os = "windows")]
 pub(crate) use windows::*;
 
-#[cfg(any(test, feature = "test-support"))]
-pub use test::TestScreenCaptureSource;
-
 #[cfg(target_os = "macos")]
 pub(crate) fn current_platform(headless: bool) -> Rc<dyn Platform> {
     Rc::new(MacPlatform::new(headless))
@@ -152,10 +149,6 @@ pub(crate) trait Platform: 'static {
         None
     }
 
-    fn screen_capture_sources(
-        &self,
-    ) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>>;
-
     fn open_window(
         &self,
         handle: AnyWindowHandle,
@@ -235,25 +228,6 @@ pub trait PlatformDisplay: Send + Sync + Debug {
     }
 }
 
-/// A source of on-screen video content that can be captured.
-pub trait ScreenCaptureSource {
-    /// Returns the video resolution of this source.
-    fn resolution(&self) -> Result<Size<Pixels>>;
-
-    /// Start capture video from this source, invoking the given callback
-    /// with each frame.
-    fn stream(
-        &self,
-        frame_callback: Box<dyn Fn(ScreenCaptureFrame)>,
-    ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
-}
-
-/// A video stream captured from a screen.
-pub trait ScreenCaptureStream {}
-
-/// A frame of video captured from a screen.
-pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
-
 /// An opaque identifier for a hardware display
 #[derive(PartialEq, Eq, Hash, Copy, Clone)]
 pub struct DisplayId(pub(crate) u32);

crates/gpui/src/platform/linux.rs 🔗

@@ -20,5 +20,3 @@ pub(crate) use text_system::*;
 pub(crate) use wayland::*;
 #[cfg(feature = "x11")]
 pub(crate) use x11::*;
-
-pub(crate) type PlatformScreenCaptureFrame = ();

crates/gpui/src/platform/linux/platform.rs 🔗

@@ -35,8 +35,8 @@ use crate::{
     px, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
     ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, Menu, MenuItem, Modifiers, OwnedMenu,
     PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInputHandler, PlatformTextSystem,
-    PlatformWindow, Point, PromptLevel, Result, ScreenCaptureSource, SemanticVersion, SharedString,
-    Size, Task, WindowAppearance, WindowOptions, WindowParams,
+    PlatformWindow, Point, PromptLevel, Result, SemanticVersion, SharedString, Size, Task,
+    WindowAppearance, WindowOptions, WindowParams,
 };
 
 pub(crate) const SCROLL_LINES: f32 = 3.0;
@@ -242,14 +242,6 @@ impl<P: LinuxClient + 'static> Platform for P {
         self.displays()
     }
 
-    fn screen_capture_sources(
-        &self,
-    ) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
-        let (mut tx, rx) = oneshot::channel();
-        tx.send(Err(anyhow!("screen capture not implemented"))).ok();
-        rx
-    }
-
     fn active_window(&self) -> Option<AnyWindowHandle> {
         self.active_window()
     }

crates/gpui/src/platform/mac.rs 🔗

@@ -4,14 +4,12 @@ mod dispatcher;
 mod display;
 mod display_link;
 mod events;
-mod screen_capture;
 
 #[cfg(not(feature = "macos-blade"))]
 mod metal_atlas;
 #[cfg(not(feature = "macos-blade"))]
 pub mod metal_renderer;
 
-use media::core_video::CVImageBuffer;
 #[cfg(not(feature = "macos-blade"))]
 use metal_renderer as renderer;
 
@@ -51,9 +49,6 @@ pub(crate) use window::*;
 #[cfg(feature = "font-kit")]
 pub(crate) use text_system::*;
 
-/// A frame of video captured from a screen.
-pub(crate) type PlatformScreenCaptureFrame = CVImageBuffer;
-
 trait BoolExt {
     fn to_objc(self) -> BOOL;
 }

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -1,14 +1,14 @@
 use super::{
     attributed_string::{NSAttributedString, NSMutableAttributedString},
     events::key_to_native,
-    renderer, screen_capture, BoolExt,
+    BoolExt,
 };
 use crate::{
     hash, Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem,
     ClipboardString, CursorStyle, ForegroundExecutor, Image, ImageFormat, Keymap, MacDispatcher,
     MacDisplay, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
-    PlatformTextSystem, PlatformWindow, Result, ScreenCaptureSource, SemanticVersion, Task,
-    WindowAppearance, WindowParams,
+    PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task, WindowAppearance,
+    WindowParams,
 };
 use anyhow::anyhow;
 use block::ConcreteBlock;
@@ -58,6 +58,8 @@ use std::{
 };
 use strum::IntoEnumIterator;
 
+use super::renderer;
+
 #[allow(non_upper_case_globals)]
 const NSUTF8StringEncoding: NSUInteger = 4;
 
@@ -548,12 +550,6 @@ impl Platform for MacPlatform {
             .collect()
     }
 
-    fn screen_capture_sources(
-        &self,
-    ) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
-        screen_capture::get_sources()
-    }
-
     fn active_window(&self) -> Option<AnyWindowHandle> {
         MacWindow::active_window()
     }

crates/gpui/src/platform/mac/screen_capture.rs 🔗

@@ -1,239 +0,0 @@
-use crate::{
-    platform::{ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream},
-    px, size, Pixels, Size,
-};
-use anyhow::{anyhow, Result};
-use block::ConcreteBlock;
-use cocoa::{
-    base::{id, nil, YES},
-    foundation::NSArray,
-};
-use core_foundation::base::TCFType;
-use ctor::ctor;
-use futures::channel::oneshot;
-use media::core_media::{CMSampleBuffer, CMSampleBufferRef};
-use metal::NSInteger;
-use objc::{
-    class,
-    declare::ClassDecl,
-    msg_send,
-    runtime::{Class, Object, Sel},
-    sel, sel_impl,
-};
-use std::{cell::RefCell, ffi::c_void, mem, ptr, rc::Rc};
-
-#[derive(Clone)]
-pub struct MacScreenCaptureSource {
-    sc_display: id,
-}
-
-pub struct MacScreenCaptureStream {
-    sc_stream: id,
-    sc_stream_output: id,
-}
-
-#[link(name = "ScreenCaptureKit", kind = "framework")]
-extern "C" {}
-
-static mut DELEGATE_CLASS: *const Class = ptr::null();
-static mut OUTPUT_CLASS: *const Class = ptr::null();
-const FRAME_CALLBACK_IVAR: &str = "frame_callback";
-
-#[allow(non_upper_case_globals)]
-const SCStreamOutputTypeScreen: NSInteger = 0;
-
-impl ScreenCaptureSource for MacScreenCaptureSource {
-    fn resolution(&self) -> Result<Size<Pixels>> {
-        unsafe {
-            let width: i64 = msg_send![self.sc_display, width];
-            let height: i64 = msg_send![self.sc_display, height];
-            Ok(size(px(width as f32), px(height as f32)))
-        }
-    }
-
-    fn stream(
-        &self,
-        frame_callback: Box<dyn Fn(ScreenCaptureFrame)>,
-    ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>> {
-        unsafe {
-            let stream: id = msg_send![class!(SCStream), alloc];
-            let filter: id = msg_send![class!(SCContentFilter), alloc];
-            let configuration: id = msg_send![class!(SCStreamConfiguration), alloc];
-            let delegate: id = msg_send![DELEGATE_CLASS, alloc];
-            let output: id = msg_send![OUTPUT_CLASS, alloc];
-
-            let excluded_windows = NSArray::array(nil);
-            let filter: id = msg_send![filter, initWithDisplay:self.sc_display excludingWindows:excluded_windows];
-            let configuration: id = msg_send![configuration, init];
-            let delegate: id = msg_send![delegate, init];
-            let output: id = msg_send![output, init];
-
-            output.as_mut().unwrap().set_ivar(
-                FRAME_CALLBACK_IVAR,
-                Box::into_raw(Box::new(frame_callback)) as *mut c_void,
-            );
-
-            let stream: id = msg_send![stream, initWithFilter:filter configuration:configuration delegate:delegate];
-
-            let (mut tx, rx) = oneshot::channel();
-
-            let mut error: id = nil;
-            let _: () = msg_send![stream, addStreamOutput:output type:SCStreamOutputTypeScreen sampleHandlerQueue:0 error:&mut error as *mut id];
-            if error != nil {
-                let message: id = msg_send![error, localizedDescription];
-                tx.send(Err(anyhow!("failed to add stream  output {message:?}")))
-                    .ok();
-                return rx;
-            }
-
-            let tx = Rc::new(RefCell::new(Some(tx)));
-            let handler = ConcreteBlock::new({
-                move |error: id| {
-                    let result = if error == nil {
-                        let stream = MacScreenCaptureStream {
-                            sc_stream: stream,
-                            sc_stream_output: output,
-                        };
-                        Ok(Box::new(stream) as Box<dyn ScreenCaptureStream>)
-                    } else {
-                        let message: id = msg_send![error, localizedDescription];
-                        Err(anyhow!("failed to stop screen capture stream {message:?}"))
-                    };
-                    if let Some(tx) = tx.borrow_mut().take() {
-                        tx.send(result).ok();
-                    }
-                }
-            });
-            let handler = handler.copy();
-            let _: () = msg_send![stream, startCaptureWithCompletionHandler:handler];
-            rx
-        }
-    }
-}
-
-impl Drop for MacScreenCaptureSource {
-    fn drop(&mut self) {
-        unsafe {
-            let _: () = msg_send![self.sc_display, release];
-        }
-    }
-}
-
-impl ScreenCaptureStream for MacScreenCaptureStream {}
-
-impl Drop for MacScreenCaptureStream {
-    fn drop(&mut self) {
-        unsafe {
-            let mut error: id = nil;
-            let _: () = msg_send![self.sc_stream, removeStreamOutput:self.sc_stream_output type:SCStreamOutputTypeScreen error:&mut error as *mut _];
-            if error != nil {
-                let message: id = msg_send![error, localizedDescription];
-                log::error!("failed to add stream  output {message:?}");
-            }
-
-            let handler = ConcreteBlock::new(move |error: id| {
-                if error != nil {
-                    let message: id = msg_send![error, localizedDescription];
-                    log::error!("failed to stop screen capture stream {message:?}");
-                }
-            });
-            let block = handler.copy();
-            let _: () = msg_send![self.sc_stream, stopCaptureWithCompletionHandler:block];
-            let _: () = msg_send![self.sc_stream, release];
-            let _: () = msg_send![self.sc_stream_output, release];
-        }
-    }
-}
-
-pub(crate) fn get_sources() -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
-    unsafe {
-        let (mut tx, rx) = oneshot::channel();
-        let tx = Rc::new(RefCell::new(Some(tx)));
-
-        let block = ConcreteBlock::new(move |shareable_content: id, error: id| {
-            let Some(mut tx) = tx.borrow_mut().take() else {
-                return;
-            };
-            let result = if error == nil {
-                let displays: id = msg_send![shareable_content, displays];
-                let mut result = Vec::new();
-                for i in 0..displays.count() {
-                    let display = displays.objectAtIndex(i);
-                    let source = MacScreenCaptureSource {
-                        sc_display: msg_send![display, retain],
-                    };
-                    result.push(Box::new(source) as Box<dyn ScreenCaptureSource>);
-                }
-                Ok(result)
-            } else {
-                let msg: id = msg_send![error, localizedDescription];
-                Err(anyhow!("Failed to register: {:?}", msg))
-            };
-            tx.send(result).ok();
-        });
-        let block = block.copy();
-
-        let _: () = msg_send![
-            class!(SCShareableContent),
-            getShareableContentExcludingDesktopWindows:YES
-                                   onScreenWindowsOnly:YES
-                                     completionHandler:block];
-        rx
-    }
-}
-
-#[ctor]
-unsafe fn build_classes() {
-    let mut decl = ClassDecl::new("GPUIStreamDelegate", class!(NSObject)).unwrap();
-    decl.add_method(
-        sel!(outputVideoEffectDidStartForStream:),
-        output_video_effect_did_start_for_stream as extern "C" fn(&Object, Sel, id),
-    );
-    decl.add_method(
-        sel!(outputVideoEffectDidStopForStream:),
-        output_video_effect_did_stop_for_stream as extern "C" fn(&Object, Sel, id),
-    );
-    decl.add_method(
-        sel!(stream:didStopWithError:),
-        stream_did_stop_with_error as extern "C" fn(&Object, Sel, id, id),
-    );
-    DELEGATE_CLASS = decl.register();
-
-    let mut decl = ClassDecl::new("GPUIStreamOutput", class!(NSObject)).unwrap();
-    decl.add_method(
-        sel!(stream:didOutputSampleBuffer:ofType:),
-        stream_did_output_sample_buffer_of_type as extern "C" fn(&Object, Sel, id, id, NSInteger),
-    );
-    decl.add_ivar::<*mut c_void>(FRAME_CALLBACK_IVAR);
-
-    OUTPUT_CLASS = decl.register();
-}
-
-extern "C" fn output_video_effect_did_start_for_stream(_this: &Object, _: Sel, _stream: id) {}
-
-extern "C" fn output_video_effect_did_stop_for_stream(_this: &Object, _: Sel, _stream: id) {}
-
-extern "C" fn stream_did_stop_with_error(_this: &Object, _: Sel, _stream: id, _error: id) {}
-
-extern "C" fn stream_did_output_sample_buffer_of_type(
-    this: &Object,
-    _: Sel,
-    _stream: id,
-    sample_buffer: id,
-    buffer_type: NSInteger,
-) {
-    if buffer_type != SCStreamOutputTypeScreen {
-        return;
-    }
-
-    unsafe {
-        let sample_buffer = sample_buffer as CMSampleBufferRef;
-        let sample_buffer = CMSampleBuffer::wrap_under_get_rule(sample_buffer);
-        if let Some(buffer) = sample_buffer.image_buffer() {
-            let callback: Box<Box<dyn Fn(ScreenCaptureFrame)>> =
-                Box::from_raw(*this.get_ivar::<*mut c_void>(FRAME_CALLBACK_IVAR) as *mut _);
-            callback(ScreenCaptureFrame(buffer));
-            mem::forget(callback);
-        }
-    }
-}

crates/gpui/src/platform/test.rs 🔗

@@ -7,5 +7,3 @@ pub(crate) use dispatcher::*;
 pub(crate) use display::*;
 pub(crate) use platform::*;
 pub(crate) use window::*;
-
-pub use platform::TestScreenCaptureSource;

crates/gpui/src/platform/test/platform.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
-    px, size, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor,
-    Keymap, Platform, PlatformDisplay, PlatformTextSystem, ScreenCaptureFrame, ScreenCaptureSource,
-    ScreenCaptureStream, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams,
+    AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor, Keymap,
+    Platform, PlatformDisplay, PlatformTextSystem, Task, TestDisplay, TestWindow, WindowAppearance,
+    WindowParams,
 };
 use anyhow::Result;
 use collections::VecDeque;
@@ -31,7 +31,6 @@ pub(crate) struct TestPlatform {
     #[cfg(any(target_os = "linux", target_os = "freebsd"))]
     current_primary_item: Mutex<Option<ClipboardItem>>,
     pub(crate) prompts: RefCell<TestPrompts>,
-    screen_capture_sources: RefCell<Vec<TestScreenCaptureSource>>,
     pub opened_url: RefCell<Option<String>>,
     pub text_system: Arc<dyn PlatformTextSystem>,
     #[cfg(target_os = "windows")]
@@ -39,31 +38,6 @@ pub(crate) struct TestPlatform {
     weak: Weak<Self>,
 }
 
-#[derive(Clone)]
-/// A fake screen capture source, used for testing.
-pub struct TestScreenCaptureSource {}
-
-pub struct TestScreenCaptureStream {}
-
-impl ScreenCaptureSource for TestScreenCaptureSource {
-    fn resolution(&self) -> Result<crate::Size<crate::Pixels>> {
-        Ok(size(px(1.), px(1.)))
-    }
-
-    fn stream(
-        &self,
-        _frame_callback: Box<dyn Fn(ScreenCaptureFrame)>,
-    ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>> {
-        let (mut tx, rx) = oneshot::channel();
-        let stream = TestScreenCaptureStream {};
-        tx.send(Ok(Box::new(stream) as Box<dyn ScreenCaptureStream>))
-            .ok();
-        rx
-    }
-}
-
-impl ScreenCaptureStream for TestScreenCaptureStream {}
-
 #[derive(Default)]
 pub(crate) struct TestPrompts {
     multiple_choice: VecDeque<oneshot::Sender<usize>>,
@@ -98,7 +72,6 @@ impl TestPlatform {
             background_executor: executor,
             foreground_executor,
             prompts: Default::default(),
-            screen_capture_sources: Default::default(),
             active_cursor: Default::default(),
             active_display: Rc::new(TestDisplay::new()),
             active_window: Default::default(),
@@ -141,10 +114,6 @@ impl TestPlatform {
         !self.prompts.borrow().multiple_choice.is_empty()
     }
 
-    pub(crate) fn set_screen_capture_sources(&self, sources: Vec<TestScreenCaptureSource>) {
-        *self.screen_capture_sources.borrow_mut() = sources;
-    }
-
     pub(crate) fn prompt(&self, msg: &str, detail: Option<&str>) -> oneshot::Receiver<usize> {
         let (tx, rx) = oneshot::channel();
         self.background_executor()
@@ -233,20 +202,6 @@ impl Platform for TestPlatform {
         Some(self.active_display.clone())
     }
 
-    fn screen_capture_sources(
-        &self,
-    ) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
-        let (mut tx, rx) = oneshot::channel();
-        tx.send(Ok(self
-            .screen_capture_sources
-            .borrow()
-            .iter()
-            .map(|source| Box::new(source.clone()) as Box<dyn ScreenCaptureSource>)
-            .collect()))
-            .ok();
-        rx
-    }
-
     fn active_window(&self) -> Option<crate::AnyWindowHandle> {
         self.active_window
             .borrow()
@@ -375,13 +330,6 @@ impl Platform for TestPlatform {
     }
 }
 
-impl TestScreenCaptureSource {
-    /// Create a fake screen capture source, for testing.
-    pub fn new() -> Self {
-        Self {}
-    }
-}
-
 #[cfg(target_os = "windows")]
 impl Drop for TestPlatform {
     fn drop(&mut self) {

crates/gpui/src/platform/windows.rs 🔗

@@ -21,5 +21,3 @@ pub(crate) use window::*;
 pub(crate) use wrapper::*;
 
 pub(crate) use windows::Win32::Foundation::HWND;
-
-pub(crate) type PlatformScreenCaptureFrame = ();

crates/gpui/src/platform/windows/platform.rs 🔗

@@ -325,14 +325,6 @@ impl Platform for WindowsPlatform {
         WindowsDisplay::primary_monitor().map(|display| Rc::new(display) as Rc<dyn PlatformDisplay>)
     }
 
-    fn screen_capture_sources(
-        &self,
-    ) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
-        let (mut tx, rx) = oneshot::channel();
-        tx.send(Err(anyhow!("screen capture not implemented"))).ok();
-        rx
-    }
-
     fn active_window(&self) -> Option<AnyWindowHandle> {
         let active_window_hwnd = unsafe { GetActiveWindow() };
         self.try_get_windows_inner_from_hwnd(active_window_hwnd)

crates/http_client/Cargo.toml 🔗

@@ -20,7 +20,7 @@ bytes.workspace = true
 anyhow.workspace = true
 derive_more.workspace = true
 futures.workspace = true
-http.workspace = true
+http = "1.1"
 log.workspace = true
 serde.workspace = true
 serde_json.workspace = true

crates/live_kit_client/Cargo.toml 🔗

@@ -2,7 +2,7 @@
 name = "live_kit_client"
 version = "0.1.0"
 edition = "2021"
-description = "Logic for using LiveKit with GPUI"
+description = "Bindings to LiveKit Swift client SDK"
 publish = false
 license = "GPL-3.0-or-later"
 
@@ -19,37 +19,42 @@ name = "test_app"
 [features]
 no-webrtc = []
 test-support = [
+    "async-trait",
     "collections/test-support",
     "gpui/test-support",
+    "live_kit_server",
     "nanoid",
 ]
 
 [dependencies]
 anyhow.workspace = true
-async-trait.workspace = true
-collections.workspace = true
-cpal = "0.15"
+async-broadcast = "0.7"
+async-trait = { workspace = true, optional = true }
+collections = { workspace = true, optional = true }
 futures.workspace = true
-gpui.workspace = true
-http_2 = { package = "http", version = "0.2.1" }
-live_kit_server.workspace = true
+gpui = { workspace = true, optional = true }
+live_kit_server = { workspace = true, optional = true }
 log.workspace = true
 media.workspace = true
 nanoid = { workspace = true, optional = true}
 parking_lot.workspace = true
 postage.workspace = true
-util.workspace = true
-http_client.workspace = true
-
-[target.'cfg(not(target_os = "windows"))'.dependencies]
-livekit.workspace = true
 
 [target.'cfg(target_os = "macos")'.dependencies]
 core-foundation.workspace = true
 
+[target.'cfg(all(not(target_os = "macos")))'.dependencies]
+async-trait = { workspace = true }
+collections = { workspace = true }
+gpui = { workspace = true }
+live_kit_server.workspace = true
+nanoid.workspace = true
+
 [dev-dependencies]
+async-trait.workspace = true
 collections = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, features = ["test-support"] }
+live_kit_server.workspace = true
 nanoid.workspace = true
 sha2.workspace = true
 simplelog.workspace = true

crates/live_kit_client/LiveKitBridge/Package.resolved 🔗

@@ -0,0 +1,52 @@
+{
+  "object": {
+    "pins": [
+      {
+        "package": "LiveKit",
+        "repositoryURL": "https://github.com/livekit/client-sdk-swift.git",
+        "state": {
+          "branch": null,
+          "revision": "7331b813a5ab8a95cfb81fb2b4ed10519428b9ff",
+          "version": "1.0.12"
+        }
+      },
+      {
+        "package": "Promises",
+        "repositoryURL": "https://github.com/google/promises.git",
+        "state": {
+          "branch": null,
+          "revision": "ec957ccddbcc710ccc64c9dcbd4c7006fcf8b73a",
+          "version": "2.2.0"
+        }
+      },
+      {
+        "package": "WebRTC",
+        "repositoryURL": "https://github.com/webrtc-sdk/Specs.git",
+        "state": {
+          "branch": null,
+          "revision": "2f6bab30c8df0fe59ab3e58bc99097f757f85f65",
+          "version": "104.5112.17"
+        }
+      },
+      {
+        "package": "swift-log",
+        "repositoryURL": "https://github.com/apple/swift-log.git",
+        "state": {
+          "branch": null,
+          "revision": "32e8d724467f8fe623624570367e3d50c5638e46",
+          "version": "1.5.2"
+        }
+      },
+      {
+        "package": "SwiftProtobuf",
+        "repositoryURL": "https://github.com/apple/swift-protobuf.git",
+        "state": {
+          "branch": null,
+          "revision": "ce20dc083ee485524b802669890291c0d8090170",
+          "version": "1.22.1"
+        }
+      }
+    ]
+  },
+  "version": 1
+}

crates/live_kit_client/LiveKitBridge/Package.swift 🔗

@@ -0,0 +1,27 @@
+// swift-tools-version: 5.5
+
+import PackageDescription
+
+let package = Package(
+    name: "LiveKitBridge",
+    platforms: [
+        .macOS(.v10_15)
+    ],
+    products: [
+        // Products define the executables and libraries a package produces, and make them visible to other packages.
+        .library(
+            name: "LiveKitBridge",
+            type: .static,
+            targets: ["LiveKitBridge"]),
+    ],
+    dependencies: [
+        .package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.0.12")),
+    ],
+    targets: [
+        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
+        // Targets can depend on other targets in this package, and on products in packages this package depends on.
+        .target(
+            name: "LiveKitBridge",
+            dependencies: [.product(name: "LiveKit", package: "client-sdk-swift")]),
+    ]
+)

crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift 🔗

@@ -0,0 +1,383 @@
+import Foundation
+import LiveKit
+import WebRTC
+import ScreenCaptureKit
+
+class LKRoomDelegate: RoomDelegate {
+    var data: UnsafeRawPointer
+    var onDidDisconnect: @convention(c) (UnsafeRawPointer) -> Void
+    var onDidSubscribeToRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void
+    var onDidUnsubscribeFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
+    var onMuteChangedFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void
+    var onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void
+    var onDidSubscribeToRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void
+    var onDidUnsubscribeFromRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
+    var onDidPublishOrUnpublishLocalAudioTrack: @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void
+    var onDidPublishOrUnpublishLocalVideoTrack: @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void
+
+    init(
+        data: UnsafeRawPointer,
+        onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void,
+        onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void,
+        onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
+        onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
+        onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void,
+        onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
+        onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
+        onDidPublishOrUnpublishLocalAudioTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void,
+        onDidPublishOrUnpublishLocalVideoTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void
+    )
+    {
+        self.data = data
+        self.onDidDisconnect = onDidDisconnect
+        self.onDidSubscribeToRemoteAudioTrack = onDidSubscribeToRemoteAudioTrack
+        self.onDidUnsubscribeFromRemoteAudioTrack = onDidUnsubscribeFromRemoteAudioTrack
+        self.onDidSubscribeToRemoteVideoTrack = onDidSubscribeToRemoteVideoTrack
+        self.onDidUnsubscribeFromRemoteVideoTrack = onDidUnsubscribeFromRemoteVideoTrack
+        self.onMuteChangedFromRemoteAudioTrack = onMuteChangedFromRemoteAudioTrack
+        self.onActiveSpeakersChanged = onActiveSpeakersChanged
+        self.onDidPublishOrUnpublishLocalAudioTrack = onDidPublishOrUnpublishLocalAudioTrack
+        self.onDidPublishOrUnpublishLocalVideoTrack = onDidPublishOrUnpublishLocalVideoTrack
+    }
+
+    func room(_ room: Room, didUpdate connectionState: ConnectionState, oldValue: ConnectionState) {
+        if connectionState.isDisconnected {
+            self.onDidDisconnect(self.data)
+        }
+    }
+
+    func room(_ room: Room, participant: RemoteParticipant, didSubscribe publication: RemoteTrackPublication, track: Track) {
+        if track.kind == .video {
+            self.onDidSubscribeToRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque())
+        } else if track.kind == .audio {
+            self.onDidSubscribeToRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque(), Unmanaged.passUnretained(publication).toOpaque())
+        }
+    }
+
+    func room(_ room: Room, participant: Participant, didUpdate publication: TrackPublication, muted: Bool) {
+        if publication.kind == .audio {
+            self.onMuteChangedFromRemoteAudioTrack(self.data, publication.sid as CFString, muted)
+        }
+    }
+
+    func room(_ room: Room, didUpdate speakers: [Participant]) {
+        guard let speaker_ids = speakers.compactMap({ $0.identity as CFString }) as CFArray? else { return }
+        self.onActiveSpeakersChanged(self.data, speaker_ids)
+    }
+
+    func room(_ room: Room, participant: RemoteParticipant, didUnsubscribe publication: RemoteTrackPublication, track: Track) {
+        if track.kind == .video {
+            self.onDidUnsubscribeFromRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString)
+        } else if track.kind == .audio {
+            self.onDidUnsubscribeFromRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString)
+        }
+    }
+
+    func room(_ room: Room, localParticipant: LocalParticipant, didPublish publication: LocalTrackPublication) {
+        if publication.kind == .video {
+            self.onDidPublishOrUnpublishLocalVideoTrack(self.data, Unmanaged.passUnretained(publication).toOpaque(), true)
+        } else if publication.kind == .audio {
+            self.onDidPublishOrUnpublishLocalAudioTrack(self.data, Unmanaged.passUnretained(publication).toOpaque(), true)
+        }
+    }
+
+    func room(_ room: Room, localParticipant: LocalParticipant, didUnpublish publication: LocalTrackPublication) {
+        if publication.kind == .video {
+            self.onDidPublishOrUnpublishLocalVideoTrack(self.data, Unmanaged.passUnretained(publication).toOpaque(), false)
+        } else if publication.kind == .audio {
+            self.onDidPublishOrUnpublishLocalAudioTrack(self.data, Unmanaged.passUnretained(publication).toOpaque(), false)
+        }
+    }
+}
+
+class LKVideoRenderer: NSObject, VideoRenderer {
+    var data: UnsafeRawPointer
+    var onFrame: @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Bool
+    var onDrop: @convention(c) (UnsafeRawPointer) -> Void
+    var adaptiveStreamIsEnabled: Bool = false
+    var adaptiveStreamSize: CGSize = .zero
+    weak var track: VideoTrack?
+
+    init(data: UnsafeRawPointer, onFrame: @escaping @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Bool, onDrop: @escaping @convention(c) (UnsafeRawPointer) -> Void) {
+        self.data = data
+        self.onFrame = onFrame
+        self.onDrop = onDrop
+    }
+
+    deinit {
+        self.onDrop(self.data)
+    }
+
+    func setSize(_ size: CGSize) {
+    }
+
+    func renderFrame(_ frame: RTCVideoFrame?) {
+        let buffer = frame?.buffer as? RTCCVPixelBuffer
+        if let pixelBuffer = buffer?.pixelBuffer {
+            if !self.onFrame(self.data, pixelBuffer) {
+                DispatchQueue.main.async {
+                    self.track?.remove(videoRenderer: self)
+                }
+            }
+        }
+    }
+}
+
+@_cdecl("LKRoomDelegateCreate")
+public func LKRoomDelegateCreate(
+    data: UnsafeRawPointer,
+    onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void,
+    onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void,
+    onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
+    onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
+    onActiveSpeakerChanged: @escaping @convention(c) (UnsafeRawPointer, CFArray) -> Void,
+    onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
+    onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
+    onDidPublishOrUnpublishLocalAudioTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void,
+    onDidPublishOrUnpublishLocalVideoTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void
+) -> UnsafeMutableRawPointer {
+    let delegate = LKRoomDelegate(
+        data: data,
+        onDidDisconnect: onDidDisconnect,
+        onDidSubscribeToRemoteAudioTrack: onDidSubscribeToRemoteAudioTrack,
+        onDidUnsubscribeFromRemoteAudioTrack: onDidUnsubscribeFromRemoteAudioTrack,
+        onMuteChangedFromRemoteAudioTrack: onMuteChangedFromRemoteAudioTrack,
+        onActiveSpeakersChanged: onActiveSpeakerChanged,
+        onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack,
+        onDidUnsubscribeFromRemoteVideoTrack: onDidUnsubscribeFromRemoteVideoTrack,
+        onDidPublishOrUnpublishLocalAudioTrack: onDidPublishOrUnpublishLocalAudioTrack,
+        onDidPublishOrUnpublishLocalVideoTrack: onDidPublishOrUnpublishLocalVideoTrack
+    )
+    return Unmanaged.passRetained(delegate).toOpaque()
+}
+
+@_cdecl("LKRoomCreate")
+public func LKRoomCreate(delegate: UnsafeRawPointer) -> UnsafeMutableRawPointer  {
+    let delegate = Unmanaged<LKRoomDelegate>.fromOpaque(delegate).takeUnretainedValue()
+    return Unmanaged.passRetained(Room(delegate: delegate)).toOpaque()
+}
+
+@_cdecl("LKRoomConnect")
+public func LKRoomConnect(room: UnsafeRawPointer, url: CFString, token: CFString, callback: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void, callback_data: UnsafeRawPointer) {
+    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
+
+    room.connect(url as String, token as String).then { _ in
+        callback(callback_data, UnsafeRawPointer(nil) as! CFString?)
+    }.catch { error in
+        callback(callback_data, error.localizedDescription as CFString)
+    }
+}
+
+@_cdecl("LKRoomDisconnect")
+public func LKRoomDisconnect(room: UnsafeRawPointer) {
+    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
+    room.disconnect()
+}
+
+@_cdecl("LKRoomPublishVideoTrack")
+public func LKRoomPublishVideoTrack(room: UnsafeRawPointer, track: UnsafeRawPointer, callback: @escaping @convention(c) (UnsafeRawPointer, UnsafeMutableRawPointer?, CFString?) -> Void, callback_data: UnsafeRawPointer) {
+    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
+    let track = Unmanaged<LocalVideoTrack>.fromOpaque(track).takeUnretainedValue()
+    room.localParticipant?.publishVideoTrack(track: track).then { publication in
+        callback(callback_data, Unmanaged.passRetained(publication).toOpaque(), nil)
+    }.catch { error in
+        callback(callback_data, nil, error.localizedDescription as CFString)
+    }
+}
+
+@_cdecl("LKRoomPublishAudioTrack")
+public func LKRoomPublishAudioTrack(room: UnsafeRawPointer, track: UnsafeRawPointer, callback: @escaping @convention(c) (UnsafeRawPointer, UnsafeMutableRawPointer?, CFString?) -> Void, callback_data: UnsafeRawPointer) {
+    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
+    let track = Unmanaged<LocalAudioTrack>.fromOpaque(track).takeUnretainedValue()
+    room.localParticipant?.publishAudioTrack(track: track).then { publication in
+        callback(callback_data, Unmanaged.passRetained(publication).toOpaque(), nil)
+    }.catch { error in
+        callback(callback_data, nil, error.localizedDescription as CFString)
+    }
+}
+
+
+@_cdecl("LKRoomUnpublishTrack")
+public func LKRoomUnpublishTrack(room: UnsafeRawPointer, publication: UnsafeRawPointer) {
+    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
+    let publication = Unmanaged<LocalTrackPublication>.fromOpaque(publication).takeUnretainedValue()
+    let _ = room.localParticipant?.unpublish(publication: publication)
+}
+
+@_cdecl("LKRoomAudioTracksForRemoteParticipant")
+public func LKRoomAudioTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
+    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
+
+    for (_, participant) in room.remoteParticipants {
+        if participant.identity == participantId as String {
+            return participant.audioTracks.compactMap { $0.track as? RemoteAudioTrack } as CFArray?
+        }
+    }
+
+    return nil;
+}
+
+@_cdecl("LKRoomAudioTrackPublicationsForRemoteParticipant")
+public func LKRoomAudioTrackPublicationsForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
+    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
+
+    for (_, participant) in room.remoteParticipants {
+        if participant.identity == participantId as String {
+            return participant.audioTracks.compactMap { $0 as? RemoteTrackPublication } as CFArray?
+        }
+    }
+
+    return nil;
+}
+
+@_cdecl("LKRoomVideoTracksForRemoteParticipant")
+public func LKRoomVideoTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
+    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
+
+    for (_, participant) in room.remoteParticipants {
+        if participant.identity == participantId as String {
+            return participant.videoTracks.compactMap { $0.track as? RemoteVideoTrack } as CFArray?
+        }
+    }
+
+    return nil;
+}
+
+@_cdecl("LKLocalAudioTrackCreateTrack")
+public func LKLocalAudioTrackCreateTrack() -> UnsafeMutableRawPointer {
+    let track = LocalAudioTrack.createTrack(options: AudioCaptureOptions(
+      echoCancellation: true,
+      noiseSuppression: true
+    ))
+
+    return Unmanaged.passRetained(track).toOpaque()
+}
+
+
+@_cdecl("LKCreateScreenShareTrackForDisplay")
+public func LKCreateScreenShareTrackForDisplay(display: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer {
+    let display = Unmanaged<MacOSDisplay>.fromOpaque(display).takeUnretainedValue()
+    let track = LocalVideoTrack.createMacOSScreenShareTrack(source: display, preferredMethod: .legacy)
+    return Unmanaged.passRetained(track).toOpaque()
+}
+
+@_cdecl("LKVideoRendererCreate")
+public func LKVideoRendererCreate(data: UnsafeRawPointer, onFrame: @escaping @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Bool, onDrop: @escaping @convention(c) (UnsafeRawPointer) -> Void) -> UnsafeMutableRawPointer {
+    Unmanaged.passRetained(LKVideoRenderer(data: data, onFrame: onFrame, onDrop: onDrop)).toOpaque()
+}
+
+@_cdecl("LKVideoTrackAddRenderer")
+public func LKVideoTrackAddRenderer(track: UnsafeRawPointer, renderer: UnsafeRawPointer) {
+    let track = Unmanaged<Track>.fromOpaque(track).takeUnretainedValue() as! VideoTrack
+    let renderer = Unmanaged<LKVideoRenderer>.fromOpaque(renderer).takeRetainedValue()
+    renderer.track = track
+    track.add(videoRenderer: renderer)
+}
+
+@_cdecl("LKRemoteVideoTrackGetSid")
+public func LKRemoteVideoTrackGetSid(track: UnsafeRawPointer) -> CFString {
+    let track = Unmanaged<RemoteVideoTrack>.fromOpaque(track).takeUnretainedValue()
+    return track.sid! as CFString
+}
+
+@_cdecl("LKRemoteAudioTrackGetSid")
+public func LKRemoteAudioTrackGetSid(track: UnsafeRawPointer) -> CFString {
+    let track = Unmanaged<RemoteAudioTrack>.fromOpaque(track).takeUnretainedValue()
+    return track.sid! as CFString
+}
+
+@_cdecl("LKRemoteAudioTrackStart")
+public func LKRemoteAudioTrackStart(track: UnsafeRawPointer) {
+    let track = Unmanaged<RemoteAudioTrack>.fromOpaque(track).takeUnretainedValue()
+    track.start()
+}
+
+@_cdecl("LKRemoteAudioTrackStop")
+public func LKRemoteAudioTrackStop(track: UnsafeRawPointer) {
+    let track = Unmanaged<RemoteAudioTrack>.fromOpaque(track).takeUnretainedValue()
+    track.stop()
+}
+
+@_cdecl("LKDisplaySources")
+public func LKDisplaySources(data: UnsafeRawPointer, callback: @escaping @convention(c) (UnsafeRawPointer, CFArray?, CFString?) -> Void) {
+    MacOSScreenCapturer.sources(for: .display, includeCurrentApplication: false, preferredMethod: .legacy).then { displaySources in
+        callback(data, displaySources as CFArray, nil)
+    }.catch { error in
+        callback(data, nil, error.localizedDescription as CFString)
+    }
+}
+
+@_cdecl("LKLocalTrackPublicationSetMute")
+public func LKLocalTrackPublicationSetMute(
+    publication: UnsafeRawPointer,
+    muted: Bool,
+    on_complete: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void,
+    callback_data: UnsafeRawPointer
+) {
+    let publication = Unmanaged<LocalTrackPublication>.fromOpaque(publication).takeUnretainedValue()
+
+    if muted {
+        publication.mute().then {
+            on_complete(callback_data, nil)
+        }.catch { error in
+            on_complete(callback_data, error.localizedDescription as CFString)
+        }
+    } else {
+        publication.unmute().then {
+            on_complete(callback_data, nil)
+        }.catch { error in
+            on_complete(callback_data, error.localizedDescription as CFString)
+        }
+    }
+}
+
+@_cdecl("LKLocalTrackPublicationIsMuted")
+public func LKLocalTrackPublicationIsMuted(
+    publication: UnsafeRawPointer
+) -> Bool {
+    let publication = Unmanaged<LocalTrackPublication>.fromOpaque(publication).takeUnretainedValue()
+    return publication.muted
+}
+
+@_cdecl("LKRemoteTrackPublicationSetEnabled")
+public func LKRemoteTrackPublicationSetEnabled(
+    publication: UnsafeRawPointer,
+    enabled: Bool,
+    on_complete: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void,
+    callback_data: UnsafeRawPointer
+) {
+    let publication = Unmanaged<RemoteTrackPublication>.fromOpaque(publication).takeUnretainedValue()
+
+    publication.set(enabled: enabled).then {
+        on_complete(callback_data, nil)
+    }.catch { error in
+        on_complete(callback_data, error.localizedDescription as CFString)
+    }
+}
+
+@_cdecl("LKRemoteTrackPublicationIsMuted")
+public func LKRemoteTrackPublicationIsMuted(
+    publication: UnsafeRawPointer
+) -> Bool {
+    let publication = Unmanaged<RemoteTrackPublication>.fromOpaque(publication).takeUnretainedValue()
+
+    return publication.muted
+}
+
+@_cdecl("LKRemoteTrackPublicationGetSid")
+public func LKRemoteTrackPublicationGetSid(
+    publication: UnsafeRawPointer
+) -> CFString {
+    let publication = Unmanaged<RemoteTrackPublication>.fromOpaque(publication).takeUnretainedValue()
+
+    return publication.sid as CFString
+}
+
+@_cdecl("LKLocalTrackPublicationGetSid")
+public func LKLocalTrackPublicationGetSid(
+    publication: UnsafeRawPointer
+) -> CFString {
+    let publication = Unmanaged<LocalTrackPublication>.fromOpaque(publication).takeUnretainedValue()
+
+    return publication.sid as CFString
+}

crates/live_kit_client/build.rs 🔗

@@ -0,0 +1,185 @@
+use serde::Deserialize;
+use std::{
+    env,
+    path::{Path, PathBuf},
+    process::Command,
+};
+
+const SWIFT_PACKAGE_NAME: &str = "LiveKitBridge";
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SwiftTargetInfo {
+    pub triple: String,
+    pub unversioned_triple: String,
+    pub module_triple: String,
+    pub swift_runtime_compatibility_version: String,
+    #[serde(rename = "librariesRequireRPath")]
+    pub libraries_require_rpath: bool,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SwiftPaths {
+    pub runtime_library_paths: Vec<String>,
+    pub runtime_library_import_paths: Vec<String>,
+    pub runtime_resource_path: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct SwiftTarget {
+    pub target: SwiftTargetInfo,
+    pub paths: SwiftPaths,
+}
+
+const MACOS_TARGET_VERSION: &str = "10.15.7";
+
+fn main() {
+    if cfg!(all(
+        target_os = "macos",
+        not(any(test, feature = "test-support", feature = "no-webrtc")),
+    )) {
+        let swift_target = get_swift_target();
+
+        build_bridge(&swift_target);
+        link_swift_stdlib(&swift_target);
+        link_webrtc_framework(&swift_target);
+
+        // Register exported Objective-C selectors, protocols, etc when building example binaries.
+        println!("cargo:rustc-link-arg=-Wl,-ObjC");
+    }
+}
+
+fn build_bridge(swift_target: &SwiftTarget) {
+    println!("cargo:rerun-if-env-changed=MACOSX_DEPLOYMENT_TARGET");
+    println!("cargo:rerun-if-changed={}/Sources", SWIFT_PACKAGE_NAME);
+    println!(
+        "cargo:rerun-if-changed={}/Package.swift",
+        SWIFT_PACKAGE_NAME
+    );
+    println!(
+        "cargo:rerun-if-changed={}/Package.resolved",
+        SWIFT_PACKAGE_NAME
+    );
+
+    let swift_package_root = swift_package_root();
+    let swift_target_folder = swift_target_folder();
+    let swift_cache_folder = swift_cache_folder();
+    if !Command::new("swift")
+        .arg("build")
+        .arg("--disable-automatic-resolution")
+        .args(["--configuration", &env::var("PROFILE").unwrap()])
+        .args(["--triple", &swift_target.target.triple])
+        .args(["--build-path".into(), swift_target_folder])
+        .args(["--cache-path".into(), swift_cache_folder])
+        .current_dir(&swift_package_root)
+        .status()
+        .unwrap()
+        .success()
+    {
+        panic!(
+            "Failed to compile swift package in {}",
+            swift_package_root.display()
+        );
+    }
+
+    println!(
+        "cargo:rustc-link-search=native={}",
+        swift_target.out_dir_path().display()
+    );
+    println!("cargo:rustc-link-lib=static={}", SWIFT_PACKAGE_NAME);
+}
+
+fn link_swift_stdlib(swift_target: &SwiftTarget) {
+    for path in &swift_target.paths.runtime_library_paths {
+        println!("cargo:rustc-link-search=native={}", path);
+    }
+}
+
+fn link_webrtc_framework(swift_target: &SwiftTarget) {
+    let swift_out_dir_path = swift_target.out_dir_path();
+    println!("cargo:rustc-link-lib=framework=WebRTC");
+    println!(
+        "cargo:rustc-link-search=framework={}",
+        swift_out_dir_path.display()
+    );
+    // Find WebRTC.framework as a sibling of the executable when running tests.
+    println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
+    // Find WebRTC.framework in parent directory of the executable when running examples.
+    println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/..");
+
+    let source_path = swift_out_dir_path.join("WebRTC.framework");
+    let deps_dir_path =
+        PathBuf::from(env::var("OUT_DIR").unwrap()).join("../../../deps/WebRTC.framework");
+    let target_dir_path =
+        PathBuf::from(env::var("OUT_DIR").unwrap()).join("../../../WebRTC.framework");
+    copy_dir(&source_path, &deps_dir_path);
+    copy_dir(&source_path, &target_dir_path);
+}
+
+fn get_swift_target() -> SwiftTarget {
+    let mut arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
+    if arch == "aarch64" {
+        arch = "arm64".into();
+    }
+    let target = format!("{}-apple-macosx{}", arch, MACOS_TARGET_VERSION);
+
+    let swift_target_info_str = Command::new("swift")
+        .args(["-target", &target, "-print-target-info"])
+        .output()
+        .unwrap()
+        .stdout;
+
+    serde_json::from_slice(&swift_target_info_str).unwrap()
+}
+
+fn swift_package_root() -> PathBuf {
+    env::current_dir().unwrap().join(SWIFT_PACKAGE_NAME)
+}
+
+fn swift_target_folder() -> PathBuf {
+    let target = env::var("TARGET").unwrap();
+    env::current_dir()
+        .unwrap()
+        .join(format!("../../target/{target}/{SWIFT_PACKAGE_NAME}_target"))
+}
+
+fn swift_cache_folder() -> PathBuf {
+    let target = env::var("TARGET").unwrap();
+    env::current_dir()
+        .unwrap()
+        .join(format!("../../target/{target}/{SWIFT_PACKAGE_NAME}_cache"))
+}
+
+fn copy_dir(source: &Path, destination: &Path) {
+    assert!(
+        Command::new("rm")
+            .arg("-rf")
+            .arg(destination)
+            .status()
+            .unwrap()
+            .success(),
+        "could not remove {:?} before copying",
+        destination
+    );
+
+    assert!(
+        Command::new("cp")
+            .arg("-R")
+            .args([source, destination])
+            .status()
+            .unwrap()
+            .success(),
+        "could not copy {:?} to {:?}",
+        source,
+        destination
+    );
+}
+
+impl SwiftTarget {
+    fn out_dir_path(&self) -> PathBuf {
+        swift_target_folder()
+            .join(&self.target.unversioned_triple)
+            .join(env::var("PROFILE").unwrap())
+    }
+}

crates/live_kit_client/examples/test_app.rs 🔗

@@ -1,53 +1,18 @@
-#![cfg_attr(windows, allow(unused))]
-
-use gpui::{
-    actions, bounds, div, point,
-    prelude::{FluentBuilder as _, IntoElement},
-    px, rgb, size, AsyncAppContext, Bounds, InteractiveElement, KeyBinding, Menu, MenuItem,
-    ParentElement, Pixels, Render, ScreenCaptureStream, SharedString,
-    StatefulInteractiveElement as _, Styled, Task, View, ViewContext, VisualContext, WindowBounds,
-    WindowHandle, WindowOptions,
-};
-#[cfg(not(target_os = "windows"))]
-use live_kit_client::{
-    capture_local_audio_track, capture_local_video_track,
-    id::ParticipantIdentity,
-    options::{TrackPublishOptions, VideoCodec},
-    participant::{Participant, RemoteParticipant},
-    play_remote_audio_track,
-    publication::{LocalTrackPublication, RemoteTrackPublication},
-    track::{LocalTrack, RemoteTrack, RemoteVideoTrack, TrackSource},
-    AudioStream, RemoteVideoTrackView, Room, RoomEvent, RoomOptions,
-};
-
-#[cfg(target_os = "windows")]
-use live_kit_client::{
-    participant::{Participant, RemoteParticipant},
-    publication::{LocalTrackPublication, RemoteTrackPublication},
-    track::{LocalTrack, RemoteTrack, RemoteVideoTrack},
-    AudioStream, RemoteVideoTrackView, Room, RoomEvent,
-};
+use std::time::Duration;
 
+use futures::StreamExt;
+use gpui::{actions, KeyBinding, Menu, MenuItem};
+use live_kit_client::{LocalAudioTrack, LocalVideoTrack, Room, RoomUpdate};
 use live_kit_server::token::{self, VideoGrant};
 use log::LevelFilter;
-use postage::stream::Stream as _;
 use simplelog::SimpleLogger;
 
 actions!(live_kit_client, [Quit]);
 
-#[cfg(windows)]
-fn main() {}
-
-#[cfg(not(windows))]
 fn main() {
     SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
 
     gpui::App::new().run(|cx| {
-        live_kit_client::init(
-            cx.background_executor().dispatcher.clone(),
-            cx.http_client(),
-        );
-
         #[cfg(any(test, feature = "test-support"))]
         println!("USING TEST LIVEKIT");
 
@@ -55,8 +20,10 @@ fn main() {
         println!("USING REAL LIVEKIT");
 
         cx.activate(true);
+
         cx.on_action(quit);
         cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
+
         cx.set_menus(vec![Menu {
             name: "Zed".into(),
             items: vec![MenuItem::Action {
@@ -69,368 +36,137 @@ fn main() {
         let live_kit_url = std::env::var("LIVE_KIT_URL").unwrap_or("http://localhost:7880".into());
         let live_kit_key = std::env::var("LIVE_KIT_KEY").unwrap_or("devkey".into());
         let live_kit_secret = std::env::var("LIVE_KIT_SECRET").unwrap_or("secret".into());
-        let height = px(800.);
-        let width = px(800.);
 
         cx.spawn(|cx| async move {
-            let mut windows = Vec::new();
-            for i in 0..3 {
-                let token = token::create(
-                    &live_kit_key,
-                    &live_kit_secret,
-                    Some(&format!("test-participant-{i}")),
-                    VideoGrant::to_join("test-room"),
-                )
-                .unwrap();
-
-                let bounds = bounds(point(width * i, px(0.0)), size(width, height));
-                let window =
-                    LivekitWindow::new(live_kit_url.as_str(), token.as_str(), bounds, cx.clone())
-                        .await;
-                windows.push(window);
-            }
-        })
-        .detach();
-    });
-}
-
-fn quit(_: &Quit, cx: &mut gpui::AppContext) {
-    cx.quit();
-}
-
-struct LivekitWindow {
-    room: Room,
-    microphone_track: Option<LocalTrackPublication>,
-    screen_share_track: Option<LocalTrackPublication>,
-    microphone_stream: Option<AudioStream>,
-    screen_share_stream: Option<Box<dyn ScreenCaptureStream>>,
-    #[cfg(not(target_os = "windows"))]
-    remote_participants: Vec<(ParticipantIdentity, ParticipantState)>,
-    _events_task: Task<()>,
-}
-
-#[derive(Default)]
-struct ParticipantState {
-    audio_output_stream: Option<(RemoteTrackPublication, AudioStream)>,
-    muted: bool,
-    screen_share_output_view: Option<(RemoteVideoTrack, View<RemoteVideoTrackView>)>,
-    speaking: bool,
-}
-
-#[cfg(not(windows))]
-impl LivekitWindow {
-    async fn new(
-        url: &str,
-        token: &str,
-        bounds: Bounds<Pixels>,
-        cx: AsyncAppContext,
-    ) -> WindowHandle<Self> {
-        let (room, mut events) = Room::connect(url, token, RoomOptions::default())
-            .await
+            let user_a_token = token::create(
+                &live_kit_key,
+                &live_kit_secret,
+                Some("test-participant-1"),
+                VideoGrant::to_join("test-room"),
+            )
             .unwrap();
-
-        cx.update(|cx| {
-            cx.open_window(
-                WindowOptions {
-                    window_bounds: Some(WindowBounds::Windowed(bounds)),
-                    ..Default::default()
-                },
-                |cx| {
-                    cx.new_view(|cx| {
-                        let _events_task = cx.spawn(|this, mut cx| async move {
-                            while let Some(event) = events.recv().await {
-                                this.update(&mut cx, |this: &mut LivekitWindow, cx| {
-                                    this.handle_room_event(event, cx)
-                                })
-                                .ok();
-                            }
-                        });
-
-                        Self {
-                            room,
-                            microphone_track: None,
-                            microphone_stream: None,
-                            screen_share_track: None,
-                            screen_share_stream: None,
-                            remote_participants: Vec::new(),
-                            _events_task,
-                        }
-                    })
-                },
+            let room_a = Room::new();
+            room_a.connect(&live_kit_url, &user_a_token).await.unwrap();
+
+            let user2_token = token::create(
+                &live_kit_key,
+                &live_kit_secret,
+                Some("test-participant-2"),
+                VideoGrant::to_join("test-room"),
             )
-            .unwrap()
-        })
-        .unwrap()
-    }
+            .unwrap();
+            let room_b = Room::new();
+            room_b.connect(&live_kit_url, &user2_token).await.unwrap();
+
+            let mut room_updates = room_b.updates();
+            let audio_track = LocalAudioTrack::create();
+            let audio_track_publication = room_a.publish_audio_track(audio_track).await.unwrap();
+
+            if let RoomUpdate::SubscribedToRemoteAudioTrack(track, _) =
+                room_updates.next().await.unwrap()
+            {
+                let remote_tracks = room_b.remote_audio_tracks("test-participant-1");
+                assert_eq!(remote_tracks.len(), 1);
+                assert_eq!(remote_tracks[0].publisher_id(), "test-participant-1");
+                assert_eq!(track.publisher_id(), "test-participant-1");
+            } else {
+                panic!("unexpected message");
+            }
 
-    fn handle_room_event(&mut self, event: RoomEvent, cx: &mut ViewContext<Self>) {
-        eprintln!("event: {event:?}");
+            audio_track_publication.set_mute(true).await.unwrap();
 
-        match event {
-            RoomEvent::TrackUnpublished {
-                publication,
-                participant,
-            } => {
-                let output = self.remote_participant(participant);
-                let unpublish_sid = publication.sid();
-                if output
-                    .audio_output_stream
-                    .as_ref()
-                    .map_or(false, |(track, _)| track.sid() == unpublish_sid)
-                {
-                    output.audio_output_stream.take();
-                }
-                if output
-                    .screen_share_output_view
-                    .as_ref()
-                    .map_or(false, |(track, _)| track.sid() == unpublish_sid)
-                {
-                    output.screen_share_output_view.take();
-                }
-                cx.notify();
+            println!("waiting for mute changed!");
+            if let RoomUpdate::RemoteAudioTrackMuteChanged { track_id, muted } =
+                room_updates.next().await.unwrap()
+            {
+                let remote_tracks = room_b.remote_audio_tracks("test-participant-1");
+                assert_eq!(remote_tracks[0].sid(), track_id);
+                assert!(muted);
+            } else {
+                panic!("unexpected message");
             }
 
-            RoomEvent::TrackSubscribed {
-                publication,
-                participant,
-                track,
-            } => {
-                let output = self.remote_participant(participant);
-                match track {
-                    RemoteTrack::Audio(track) => {
-                        output.audio_output_stream =
-                            Some((publication.clone(), play_remote_audio_track(&track, cx)));
-                    }
-                    RemoteTrack::Video(track) => {
-                        output.screen_share_output_view = Some((
-                            track.clone(),
-                            cx.new_view(|cx| RemoteVideoTrackView::new(track, cx)),
-                        ));
-                    }
-                }
-                cx.notify();
-            }
+            audio_track_publication.set_mute(false).await.unwrap();
 
-            RoomEvent::TrackMuted { participant, .. } => {
-                if let Participant::Remote(participant) = participant {
-                    self.remote_participant(participant).muted = true;
-                    cx.notify();
-                }
+            if let RoomUpdate::RemoteAudioTrackMuteChanged { track_id, muted } =
+                room_updates.next().await.unwrap()
+            {
+                let remote_tracks = room_b.remote_audio_tracks("test-participant-1");
+                assert_eq!(remote_tracks[0].sid(), track_id);
+                assert!(!muted);
+            } else {
+                panic!("unexpected message");
             }
 
-            RoomEvent::TrackUnmuted { participant, .. } => {
-                if let Participant::Remote(participant) = participant {
-                    self.remote_participant(participant).muted = false;
-                    cx.notify();
-                }
-            }
+            println!("Pausing for 5 seconds to test audio, make some noise!");
+            let timer = cx.background_executor().timer(Duration::from_secs(5));
+            timer.await;
+            let remote_audio_track = room_b
+                .remote_audio_tracks("test-participant-1")
+                .pop()
+                .unwrap();
+            room_a.unpublish_track(audio_track_publication);
 
-            RoomEvent::ActiveSpeakersChanged { speakers } => {
-                for (identity, output) in &mut self.remote_participants {
-                    output.speaking = speakers.iter().any(|speaker| {
-                        if let Participant::Remote(speaker) = speaker {
-                            speaker.identity() == *identity
-                        } else {
-                            false
-                        }
-                    });
-                }
-                cx.notify();
+            // Clear out any active speakers changed messages
+            let mut next = room_updates.next().await.unwrap();
+            while let RoomUpdate::ActiveSpeakersChanged { speakers } = next {
+                println!("Speakers changed: {:?}", speakers);
+                next = room_updates.next().await.unwrap();
             }
 
-            _ => {}
-        }
+            if let RoomUpdate::UnsubscribedFromRemoteAudioTrack {
+                publisher_id,
+                track_id,
+            } = next
+            {
+                assert_eq!(publisher_id, "test-participant-1");
+                assert_eq!(remote_audio_track.sid(), track_id);
+                assert_eq!(room_b.remote_audio_tracks("test-participant-1").len(), 0);
+            } else {
+                panic!("unexpected message");
+            }
 
-        cx.notify();
-    }
+            let displays = room_a.display_sources().await.unwrap();
+            let display = displays.into_iter().next().unwrap();
 
-    fn remote_participant(&mut self, participant: RemoteParticipant) -> &mut ParticipantState {
-        match self
-            .remote_participants
-            .binary_search_by_key(&&participant.identity(), |row| &row.0)
-        {
-            Ok(ix) => &mut self.remote_participants[ix].1,
-            Err(ix) => {
-                self.remote_participants
-                    .insert(ix, (participant.identity(), ParticipantState::default()));
-                &mut self.remote_participants[ix].1
-            }
-        }
-    }
+            let local_video_track = LocalVideoTrack::screen_share_for_display(&display);
+            let local_video_track_publication =
+                room_a.publish_video_track(local_video_track).await.unwrap();
 
-    fn toggle_mute(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(track) = &self.microphone_track {
-            if track.is_muted() {
-                track.unmute();
+            if let RoomUpdate::SubscribedToRemoteVideoTrack(track) =
+                room_updates.next().await.unwrap()
+            {
+                let remote_video_tracks = room_b.remote_video_tracks("test-participant-1");
+                assert_eq!(remote_video_tracks.len(), 1);
+                assert_eq!(remote_video_tracks[0].publisher_id(), "test-participant-1");
+                assert_eq!(track.publisher_id(), "test-participant-1");
             } else {
-                track.mute();
+                panic!("unexpected message");
             }
-            cx.notify();
-        } else {
-            let participant = self.room.local_participant();
-            cx.spawn(|this, mut cx| async move {
-                let (track, stream) = cx.update(|cx| capture_local_audio_track(cx))??;
-                let publication = participant
-                    .publish_track(
-                        LocalTrack::Audio(track),
-                        TrackPublishOptions {
-                            source: TrackSource::Microphone,
-                            ..Default::default()
-                        },
-                    )
-                    .await
-                    .unwrap();
-                this.update(&mut cx, |this, cx| {
-                    this.microphone_track = Some(publication);
-                    this.microphone_stream = Some(stream);
-                    cx.notify();
-                })
-            })
-            .detach();
-        }
-    }
-
-    fn toggle_screen_share(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(track) = self.screen_share_track.take() {
-            self.screen_share_stream.take();
-            let participant = self.room.local_participant();
-            cx.background_executor()
-                .spawn(async move {
-                    participant.unpublish_track(&track.sid()).await.unwrap();
-                })
-                .detach();
-            cx.notify();
-        } else {
-            let participant = self.room.local_participant();
-            let sources = cx.screen_capture_sources();
-            cx.spawn(|this, mut cx| async move {
-                let sources = sources.await.unwrap()?;
-                let source = sources.into_iter().next().unwrap();
-                let (track, stream) = capture_local_video_track(&*source).await?;
-                let publication = participant
-                    .publish_track(
-                        LocalTrack::Video(track),
-                        TrackPublishOptions {
-                            source: TrackSource::Screenshare,
-                            video_codec: VideoCodec::H264,
-                            ..Default::default()
-                        },
-                    )
-                    .await
-                    .unwrap();
-                this.update(&mut cx, |this, cx| {
-                    this.screen_share_track = Some(publication);
-                    this.screen_share_stream = Some(stream);
-                    cx.notify();
-                })
-            })
-            .detach();
-        }
-    }
 
-    fn toggle_remote_audio_for_participant(
-        &mut self,
-        identity: &ParticipantIdentity,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<()> {
-        let participant = self.remote_participants.iter().find_map(|(id, state)| {
-            if id == identity {
-                Some(state)
+            let remote_video_track = room_b
+                .remote_video_tracks("test-participant-1")
+                .pop()
+                .unwrap();
+            room_a.unpublish_track(local_video_track_publication);
+            if let RoomUpdate::UnsubscribedFromRemoteVideoTrack {
+                publisher_id,
+                track_id,
+            } = room_updates.next().await.unwrap()
+            {
+                assert_eq!(publisher_id, "test-participant-1");
+                assert_eq!(remote_video_track.sid(), track_id);
+                assert_eq!(room_b.remote_video_tracks("test-participant-1").len(), 0);
             } else {
-                None
+                panic!("unexpected message");
             }
-        })?;
-        let publication = &participant.audio_output_stream.as_ref()?.0;
-        publication.set_enabled(!publication.is_enabled());
-        cx.notify();
-        Some(())
-    }
-}
 
-#[cfg(not(windows))]
-impl Render for LivekitWindow {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        fn button() -> gpui::Div {
-            div()
-                .w(px(180.0))
-                .h(px(30.0))
-                .px_2()
-                .m_2()
-                .bg(rgb(0x8888ff))
-        }
+            cx.update(|cx| cx.shutdown()).ok();
+        })
+        .detach();
+    });
+}
 
-        div()
-            .bg(rgb(0xffffff))
-            .size_full()
-            .flex()
-            .flex_col()
-            .child(
-                div().bg(rgb(0xffd4a8)).flex().flex_row().children([
-                    button()
-                        .id("toggle-mute")
-                        .child(if let Some(track) = &self.microphone_track {
-                            if track.is_muted() {
-                                "Unmute"
-                            } else {
-                                "Mute"
-                            }
-                        } else {
-                            "Publish mic"
-                        })
-                        .on_click(cx.listener(|this, _, cx| this.toggle_mute(cx))),
-                    button()
-                        .id("toggle-screen-share")
-                        .child(if self.screen_share_track.is_none() {
-                            "Share screen"
-                        } else {
-                            "Unshare screen"
-                        })
-                        .on_click(cx.listener(|this, _, cx| this.toggle_screen_share(cx))),
-                ]),
-            )
-            .child(
-                div()
-                    .id("remote-participants")
-                    .overflow_y_scroll()
-                    .flex()
-                    .flex_col()
-                    .flex_grow()
-                    .children(self.remote_participants.iter().map(|(identity, state)| {
-                        div()
-                            .h(px(300.0))
-                            .flex()
-                            .flex_col()
-                            .m_2()
-                            .px_2()
-                            .bg(rgb(0x8888ff))
-                            .child(SharedString::from(if state.speaking {
-                                format!("{} (speaking)", &identity.0)
-                            } else if state.muted {
-                                format!("{} (muted)", &identity.0)
-                            } else {
-                                identity.0.clone()
-                            }))
-                            .when_some(state.audio_output_stream.as_ref(), |el, state| {
-                                el.child(
-                                    button()
-                                        .id(SharedString::from(identity.0.clone()))
-                                        .child(if state.0.is_enabled() {
-                                            "Deafen"
-                                        } else {
-                                            "Undeafen"
-                                        })
-                                        .on_click(cx.listener({
-                                            let identity = identity.clone();
-                                            move |this, _, cx| {
-                                                this.toggle_remote_audio_for_participant(
-                                                    &identity, cx,
-                                                );
-                                            }
-                                        })),
-                                )
-                            })
-                            .children(state.screen_share_output_view.as_ref().map(|e| e.1.clone()))
-                    })),
-            )
-    }
+fn quit(_: &Quit, cx: &mut gpui::AppContext) {
+    cx.quit();
 }

crates/live_kit_client/src/live_kit_client.rs 🔗

@@ -1,387 +1,37 @@
-#![cfg_attr(target_os = "windows", allow(unused))]
+#![allow(clippy::arc_with_non_send_sync)]
 
-mod remote_video_track_view;
-#[cfg(any(test, feature = "test-support", target_os = "windows"))]
-pub mod test;
-
-use anyhow::{anyhow, Context as _, Result};
-use cpal::{
-    traits::{DeviceTrait, HostTrait, StreamTrait as _},
-    StreamConfig,
-};
-use futures::{io, Stream, StreamExt as _};
-use gpui::{AppContext, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, Task};
-use parking_lot::Mutex;
-use std::{borrow::Cow, future::Future, pin::Pin, sync::Arc};
-use util::{debug_panic, ResultExt as _, TryFutureExt};
-#[cfg(not(target_os = "windows"))]
-use webrtc::{
-    audio_frame::AudioFrame,
-    audio_source::{native::NativeAudioSource, AudioSourceOptions, RtcAudioSource},
-    audio_stream::native::NativeAudioStream,
-    video_frame::{VideoBuffer, VideoFrame, VideoRotation},
-    video_source::{native::NativeVideoSource, RtcVideoSource, VideoResolution},
-    video_stream::native::NativeVideoStream,
-};
-
-#[cfg(all(not(any(test, feature = "test-support")), not(target_os = "windows")))]
-pub use livekit::*;
-#[cfg(any(test, feature = "test-support", target_os = "windows"))]
-pub use test::*;
-
-pub use remote_video_track_view::{RemoteVideoTrackView, RemoteVideoTrackViewEvent};
-
-pub struct AudioStream {
-    _tasks: [Task<Option<()>>; 2],
-}
-
-struct Dispatcher(Arc<dyn gpui::PlatformDispatcher>);
-
-#[cfg(not(target_os = "windows"))]
-impl livekit::dispatcher::Dispatcher for Dispatcher {
-    fn dispatch(&self, runnable: livekit::dispatcher::Runnable) {
-        self.0.dispatch(runnable, None);
-    }
-
-    fn dispatch_after(
-        &self,
-        duration: std::time::Duration,
-        runnable: livekit::dispatcher::Runnable,
-    ) {
-        self.0.dispatch_after(duration, runnable);
-    }
-}
-
-struct HttpClientAdapter(Arc<dyn http_client::HttpClient>);
-
-fn http_2_status(status: http_client::http::StatusCode) -> http_2::StatusCode {
-    http_2::StatusCode::from_u16(status.as_u16())
-        .expect("valid status code to status code conversion")
-}
-
-#[cfg(not(target_os = "windows"))]
-impl livekit::dispatcher::HttpClient for HttpClientAdapter {
-    fn get(
-        &self,
-        url: &str,
-    ) -> Pin<Box<dyn Future<Output = io::Result<livekit::dispatcher::Response>> + Send>> {
-        let http_client = self.0.clone();
-        let url = url.to_string();
-        Box::pin(async move {
-            let response = http_client
-                .get(&url, http_client::AsyncBody::empty(), false)
-                .await
-                .map_err(io::Error::other)?;
-            Ok(livekit::dispatcher::Response {
-                status: http_2_status(response.status()),
-                body: Box::pin(response.into_body()),
-            })
-        })
-    }
-
-    fn send_async(
-        &self,
-        request: http_2::Request<Vec<u8>>,
-    ) -> Pin<Box<dyn Future<Output = io::Result<livekit::dispatcher::Response>> + Send>> {
-        let http_client = self.0.clone();
-        let mut builder = http_client::http::Request::builder()
-            .method(request.method().as_str())
-            .uri(request.uri().to_string());
-
-        for (key, value) in request.headers().iter() {
-            builder = builder.header(key.as_str(), value.as_bytes());
-        }
-
-        if !request.extensions().is_empty() {
-            debug_panic!(
-                "Livekit sent an HTTP request with a protocol extension that Zed doesn't support!"
-            );
-        }
-
-        let request = builder
-            .body(http_client::AsyncBody::from_bytes(
-                request.into_body().into(),
-            ))
-            .unwrap();
-
-        Box::pin(async move {
-            let response = http_client.send(request).await.map_err(io::Error::other)?;
-            Ok(livekit::dispatcher::Response {
-                status: http_2_status(response.status()),
-                body: Box::pin(response.into_body()),
-            })
-        })
-    }
-}
-
-#[cfg(target_os = "windows")]
-pub fn init(
-    dispatcher: Arc<dyn gpui::PlatformDispatcher>,
-    http_client: Arc<dyn http_client::HttpClient>,
-) {
-}
-
-#[cfg(not(target_os = "windows"))]
-pub fn init(
-    dispatcher: Arc<dyn gpui::PlatformDispatcher>,
-    http_client: Arc<dyn http_client::HttpClient>,
-) {
-    livekit::dispatcher::set_dispatcher(Dispatcher(dispatcher));
-    livekit::dispatcher::set_http_client(HttpClientAdapter(http_client));
-}
-
-#[cfg(not(target_os = "windows"))]
-pub async fn capture_local_video_track(
-    capture_source: &dyn ScreenCaptureSource,
-) -> Result<(track::LocalVideoTrack, Box<dyn ScreenCaptureStream>)> {
-    let resolution = capture_source.resolution()?;
-    let track_source = NativeVideoSource::new(VideoResolution {
-        width: resolution.width.0 as u32,
-        height: resolution.height.0 as u32,
-    });
-
-    let capture_stream = capture_source
-        .stream({
-            let track_source = track_source.clone();
-            Box::new(move |frame| {
-                if let Some(buffer) = video_frame_buffer_to_webrtc(frame) {
-                    track_source.capture_frame(&VideoFrame {
-                        rotation: VideoRotation::VideoRotation0,
-                        timestamp_us: 0,
-                        buffer,
-                    });
-                }
-            })
-        })
-        .await??;
-
-    Ok((
-        track::LocalVideoTrack::create_video_track(
-            "screen share",
-            RtcVideoSource::Native(track_source),
-        ),
-        capture_stream,
-    ))
-}
-
-#[cfg(not(target_os = "windows"))]
-pub fn capture_local_audio_track(
-    cx: &mut AppContext,
-) -> Result<(track::LocalAudioTrack, AudioStream)> {
-    let (frame_tx, mut frame_rx) = futures::channel::mpsc::unbounded();
+use std::sync::Arc;
 
-    let sample_rate;
-    let channels;
-    let stream;
-    if cfg!(any(test, feature = "test-support")) {
-        sample_rate = 1;
-        channels = 1;
-        stream = None;
-    } else {
-        let device = cpal::default_host()
-            .default_input_device()
-            .ok_or_else(|| anyhow!("no audio input device available"))?;
-        let config = device
-            .default_input_config()
-            .context("failed to get default input config")?;
-        sample_rate = config.sample_rate().0;
-        channels = config.channels() as u32;
-        stream = Some(
-            device
-                .build_input_stream_raw(
-                    &config.config(),
-                    cpal::SampleFormat::I16,
-                    move |data, _: &_| {
-                        frame_tx
-                            .unbounded_send(AudioFrame {
-                                data: Cow::Owned(data.as_slice::<i16>().unwrap().to_vec()),
-                                sample_rate,
-                                num_channels: channels,
-                                samples_per_channel: data.len() as u32 / channels,
-                            })
-                            .ok();
-                    },
-                    |err| log::error!("error capturing audio track: {:?}", err),
-                    None,
-                )
-                .context("failed to build input stream")?,
-        );
-    }
+#[cfg(all(target_os = "macos", not(any(test, feature = "test-support"))))]
+pub mod prod;
 
-    let source = NativeAudioSource::new(
-        AudioSourceOptions {
-            echo_cancellation: true,
-            noise_suppression: true,
-            auto_gain_control: false,
-        },
-        sample_rate,
-        channels,
-        // TODO livekit: Pull these out of a proto later
-        100,
-    );
+#[cfg(all(target_os = "macos", not(any(test, feature = "test-support"))))]
+pub use prod::*;
 
-    let stream_task = cx.foreground_executor().spawn(async move {
-        if let Some(stream) = &stream {
-            stream.play().log_err();
-        }
-        futures::future::pending().await
-    });
-
-    let transmit_task = cx.background_executor().spawn({
-        let source = source.clone();
-        async move {
-            while let Some(frame) = frame_rx.next().await {
-                source.capture_frame(&frame).await.ok();
-            }
-            Some(())
-        }
-    });
-
-    let track =
-        track::LocalAudioTrack::create_audio_track("microphone", RtcAudioSource::Native(source));
-
-    Ok((
-        track,
-        AudioStream {
-            _tasks: [stream_task, transmit_task],
-        },
-    ))
-}
-
-#[cfg(not(target_os = "windows"))]
-pub fn play_remote_audio_track(
-    track: &track::RemoteAudioTrack,
-    cx: &mut AppContext,
-) -> AudioStream {
-    let buffer = Arc::new(Mutex::new(Vec::<i16>::new()));
-    let (stream_config_tx, mut stream_config_rx) = futures::channel::mpsc::unbounded();
-    // TODO livekit: Pull these out of a proto later
-    let mut stream = NativeAudioStream::new(track.rtc_track(), 48000, 1);
-
-    let receive_task = cx.background_executor().spawn({
-        let buffer = buffer.clone();
-        async move {
-            let mut stream_config = None;
-            while let Some(frame) = stream.next().await {
-                let mut buffer = buffer.lock();
-                let buffer_size = frame.samples_per_channel * frame.num_channels;
-                debug_assert!(frame.data.len() == buffer_size as usize);
-
-                let frame_config = StreamConfig {
-                    channels: frame.num_channels as u16,
-                    sample_rate: cpal::SampleRate(frame.sample_rate),
-                    buffer_size: cpal::BufferSize::Fixed(buffer_size),
-                };
-
-                if stream_config.as_ref().map_or(true, |c| *c != frame_config) {
-                    buffer.resize(buffer_size as usize, 0);
-                    stream_config = Some(frame_config.clone());
-                    stream_config_tx.unbounded_send(frame_config).ok();
-                }
-
-                if frame.data.len() == buffer.len() {
-                    buffer.copy_from_slice(&frame.data);
-                } else {
-                    buffer.iter_mut().for_each(|x| *x = 0);
-                }
-            }
-            Some(())
-        }
-    });
-
-    let play_task = cx.foreground_executor().spawn(
-        {
-            let buffer = buffer.clone();
-            async move {
-                if cfg!(any(test, feature = "test-support")) {
-                    return Err(anyhow!("can't play audio in tests"));
-                }
-
-                let device = cpal::default_host()
-                    .default_output_device()
-                    .ok_or_else(|| anyhow!("no audio output device available"))?;
-
-                let mut _output_stream = None;
-                while let Some(config) = stream_config_rx.next().await {
-                    _output_stream = Some(device.build_output_stream(
-                        &config,
-                        {
-                            let buffer = buffer.clone();
-                            move |data, _info| {
-                                let buffer = buffer.lock();
-                                if data.len() == buffer.len() {
-                                    data.copy_from_slice(&buffer);
-                                } else {
-                                    data.iter_mut().for_each(|x| *x = 0);
-                                }
-                            }
-                        },
-                        |error| log::error!("error playing audio track: {:?}", error),
-                        None,
-                    )?);
-                }
-
-                Ok(())
-            }
-        }
-        .log_err(),
-    );
-
-    AudioStream {
-        _tasks: [receive_task, play_task],
-    }
-}
-
-#[cfg(target_os = "windows")]
-pub fn play_remote_video_track(
-    track: &track::RemoteVideoTrack,
-) -> impl Stream<Item = ScreenCaptureFrame> {
-    futures::stream::empty()
-}
-
-#[cfg(not(target_os = "windows"))]
-pub fn play_remote_video_track(
-    track: &track::RemoteVideoTrack,
-) -> impl Stream<Item = ScreenCaptureFrame> {
-    NativeVideoStream::new(track.rtc_track())
-        .filter_map(|frame| async move { video_frame_buffer_from_webrtc(frame.buffer) })
-}
-
-#[cfg(target_os = "macos")]
-fn video_frame_buffer_from_webrtc(buffer: Box<dyn VideoBuffer>) -> Option<ScreenCaptureFrame> {
-    use core_foundation::base::TCFType as _;
-    use media::core_video::CVImageBuffer;
-
-    let buffer = buffer.as_native()?;
-    let pixel_buffer = buffer.get_cv_pixel_buffer();
-    if pixel_buffer.is_null() {
-        return None;
-    }
-
-    unsafe {
-        Some(ScreenCaptureFrame(CVImageBuffer::wrap_under_get_rule(
-            pixel_buffer as _,
-        )))
-    }
-}
-
-#[cfg(not(any(target_os = "macos", target_os = "windows")))]
-fn video_frame_buffer_from_webrtc(_buffer: Box<dyn VideoBuffer>) -> Option<ScreenCaptureFrame> {
-    None
-}
-
-#[cfg(target_os = "macos")]
-fn video_frame_buffer_to_webrtc(frame: ScreenCaptureFrame) -> Option<impl AsRef<dyn VideoBuffer>> {
-    use core_foundation::base::TCFType as _;
+#[cfg(any(test, feature = "test-support", not(target_os = "macos")))]
+pub mod test;
 
-    let pixel_buffer = frame.0.as_concrete_TypeRef();
-    std::mem::forget(frame.0);
-    unsafe {
-        Some(webrtc::video_frame::native::NativeBuffer::from_cv_pixel_buffer(pixel_buffer as _))
-    }
-}
+#[cfg(any(test, feature = "test-support", not(target_os = "macos")))]
+pub use test::*;
 
-#[cfg(not(any(target_os = "macos", target_os = "windows")))]
-fn video_frame_buffer_to_webrtc(_frame: ScreenCaptureFrame) -> Option<impl AsRef<dyn VideoBuffer>> {
-    None as Option<Box<dyn VideoBuffer>>
+pub type Sid = String;
+
+#[derive(Clone, Eq, PartialEq)]
+pub enum ConnectionState {
+    Disconnected,
+    Connected { url: String, token: String },
+}
+
+#[derive(Clone)]
+pub enum RoomUpdate {
+    ActiveSpeakersChanged { speakers: Vec<Sid> },
+    RemoteAudioTrackMuteChanged { track_id: Sid, muted: bool },
+    SubscribedToRemoteVideoTrack(Arc<RemoteVideoTrack>),
+    SubscribedToRemoteAudioTrack(Arc<RemoteAudioTrack>, Arc<RemoteTrackPublication>),
+    UnsubscribedFromRemoteVideoTrack { publisher_id: Sid, track_id: Sid },
+    UnsubscribedFromRemoteAudioTrack { publisher_id: Sid, track_id: Sid },
+    LocalAudioTrackPublished { publication: LocalTrackPublication },
+    LocalAudioTrackUnpublished { publication: LocalTrackPublication },
+    LocalVideoTrackPublished { publication: LocalTrackPublication },
+    LocalVideoTrackUnpublished { publication: LocalTrackPublication },
 }

crates/live_kit_client/src/prod.rs 🔗

@@ -0,0 +1,981 @@
+use crate::{ConnectionState, RoomUpdate, Sid};
+use anyhow::{anyhow, Context, Result};
+use core_foundation::{
+    array::{CFArray, CFArrayRef},
+    base::{CFRelease, CFRetain, TCFType},
+    string::{CFString, CFStringRef},
+};
+use futures::{
+    channel::{mpsc, oneshot},
+    Future,
+};
+pub use media::core_video::CVImageBuffer;
+use media::core_video::CVImageBufferRef;
+use parking_lot::Mutex;
+use postage::watch;
+use std::{
+    ffi::c_void,
+    sync::{Arc, Weak},
+};
+
+macro_rules! pointer_type {
+    ($pointer_name:ident) => {
+        #[repr(transparent)]
+        #[derive(Copy, Clone, Debug)]
+        pub struct $pointer_name(pub *const std::ffi::c_void);
+        unsafe impl Send for $pointer_name {}
+    };
+}
+
+mod swift {
+    pointer_type!(Room);
+    pointer_type!(LocalAudioTrack);
+    pointer_type!(RemoteAudioTrack);
+    pointer_type!(LocalVideoTrack);
+    pointer_type!(RemoteVideoTrack);
+    pointer_type!(LocalTrackPublication);
+    pointer_type!(RemoteTrackPublication);
+    pointer_type!(MacOSDisplay);
+    pointer_type!(RoomDelegate);
+}
+
+extern "C" {
+    fn LKRoomDelegateCreate(
+        callback_data: *mut c_void,
+        on_did_disconnect: extern "C" fn(callback_data: *mut c_void),
+        on_did_subscribe_to_remote_audio_track: extern "C" fn(
+            callback_data: *mut c_void,
+            publisher_id: CFStringRef,
+            track_id: CFStringRef,
+            remote_track: swift::RemoteAudioTrack,
+            remote_publication: swift::RemoteTrackPublication,
+        ),
+        on_did_unsubscribe_from_remote_audio_track: extern "C" fn(
+            callback_data: *mut c_void,
+            publisher_id: CFStringRef,
+            track_id: CFStringRef,
+        ),
+        on_mute_changed_from_remote_audio_track: extern "C" fn(
+            callback_data: *mut c_void,
+            track_id: CFStringRef,
+            muted: bool,
+        ),
+        on_active_speakers_changed: extern "C" fn(
+            callback_data: *mut c_void,
+            participants: CFArrayRef,
+        ),
+        on_did_subscribe_to_remote_video_track: extern "C" fn(
+            callback_data: *mut c_void,
+            publisher_id: CFStringRef,
+            track_id: CFStringRef,
+            remote_track: swift::RemoteVideoTrack,
+        ),
+        on_did_unsubscribe_from_remote_video_track: extern "C" fn(
+            callback_data: *mut c_void,
+            publisher_id: CFStringRef,
+            track_id: CFStringRef,
+        ),
+        on_did_publish_or_unpublish_local_audio_track: extern "C" fn(
+            callback_data: *mut c_void,
+            publication: swift::LocalTrackPublication,
+            is_published: bool,
+        ),
+        on_did_publish_or_unpublish_local_video_track: extern "C" fn(
+            callback_data: *mut c_void,
+            publication: swift::LocalTrackPublication,
+            is_published: bool,
+        ),
+    ) -> swift::RoomDelegate;
+
+    fn LKRoomCreate(delegate: swift::RoomDelegate) -> swift::Room;
+    fn LKRoomConnect(
+        room: swift::Room,
+        url: CFStringRef,
+        token: CFStringRef,
+        callback: extern "C" fn(*mut c_void, CFStringRef),
+        callback_data: *mut c_void,
+    );
+    fn LKRoomDisconnect(room: swift::Room);
+    fn LKRoomPublishVideoTrack(
+        room: swift::Room,
+        track: swift::LocalVideoTrack,
+        callback: extern "C" fn(*mut c_void, swift::LocalTrackPublication, CFStringRef),
+        callback_data: *mut c_void,
+    );
+    fn LKRoomPublishAudioTrack(
+        room: swift::Room,
+        track: swift::LocalAudioTrack,
+        callback: extern "C" fn(*mut c_void, swift::LocalTrackPublication, CFStringRef),
+        callback_data: *mut c_void,
+    );
+    fn LKRoomUnpublishTrack(room: swift::Room, publication: swift::LocalTrackPublication);
+
+    fn LKRoomAudioTracksForRemoteParticipant(
+        room: swift::Room,
+        participant_id: CFStringRef,
+    ) -> CFArrayRef;
+
+    fn LKRoomAudioTrackPublicationsForRemoteParticipant(
+        room: swift::Room,
+        participant_id: CFStringRef,
+    ) -> CFArrayRef;
+
+    fn LKRoomVideoTracksForRemoteParticipant(
+        room: swift::Room,
+        participant_id: CFStringRef,
+    ) -> CFArrayRef;
+
+    fn LKVideoRendererCreate(
+        callback_data: *mut c_void,
+        on_frame: extern "C" fn(callback_data: *mut c_void, frame: CVImageBufferRef) -> bool,
+        on_drop: extern "C" fn(callback_data: *mut c_void),
+    ) -> *const c_void;
+
+    fn LKRemoteAudioTrackGetSid(track: swift::RemoteAudioTrack) -> CFStringRef;
+    fn LKRemoteVideoTrackGetSid(track: swift::RemoteVideoTrack) -> CFStringRef;
+    fn LKRemoteAudioTrackStart(track: swift::RemoteAudioTrack);
+    fn LKRemoteAudioTrackStop(track: swift::RemoteAudioTrack);
+    fn LKVideoTrackAddRenderer(track: swift::RemoteVideoTrack, renderer: *const c_void);
+
+    fn LKDisplaySources(
+        callback_data: *mut c_void,
+        callback: extern "C" fn(
+            callback_data: *mut c_void,
+            sources: CFArrayRef,
+            error: CFStringRef,
+        ),
+    );
+    fn LKCreateScreenShareTrackForDisplay(display: swift::MacOSDisplay) -> swift::LocalVideoTrack;
+    fn LKLocalAudioTrackCreateTrack() -> swift::LocalAudioTrack;
+
+    fn LKLocalTrackPublicationSetMute(
+        publication: swift::LocalTrackPublication,
+        muted: bool,
+        on_complete: extern "C" fn(callback_data: *mut c_void, error: CFStringRef),
+        callback_data: *mut c_void,
+    );
+
+    fn LKRemoteTrackPublicationSetEnabled(
+        publication: swift::RemoteTrackPublication,
+        enabled: bool,
+        on_complete: extern "C" fn(callback_data: *mut c_void, error: CFStringRef),
+        callback_data: *mut c_void,
+    );
+
+    fn LKLocalTrackPublicationIsMuted(publication: swift::LocalTrackPublication) -> bool;
+    fn LKRemoteTrackPublicationIsMuted(publication: swift::RemoteTrackPublication) -> bool;
+    fn LKLocalTrackPublicationGetSid(publication: swift::LocalTrackPublication) -> CFStringRef;
+    fn LKRemoteTrackPublicationGetSid(publication: swift::RemoteTrackPublication) -> CFStringRef;
+}
+
+pub struct Room {
+    native_room: swift::Room,
+    connection: Mutex<(
+        watch::Sender<ConnectionState>,
+        watch::Receiver<ConnectionState>,
+    )>,
+    update_subscribers: Mutex<Vec<mpsc::UnboundedSender<RoomUpdate>>>,
+    _delegate: RoomDelegate,
+}
+
+impl Room {
+    pub fn new() -> Arc<Self> {
+        Arc::new_cyclic(|weak_room| {
+            let delegate = RoomDelegate::new(weak_room.clone());
+            Self {
+                native_room: unsafe { LKRoomCreate(delegate.native_delegate) },
+                connection: Mutex::new(watch::channel_with(ConnectionState::Disconnected)),
+                update_subscribers: Default::default(),
+                _delegate: delegate,
+            }
+        })
+    }
+
+    pub fn status(&self) -> watch::Receiver<ConnectionState> {
+        self.connection.lock().1.clone()
+    }
+
+    pub fn connect(self: &Arc<Self>, url: &str, token: &str) -> impl Future<Output = Result<()>> {
+        let url = CFString::new(url);
+        let token = CFString::new(token);
+        let (did_connect, tx, rx) = Self::build_done_callback();
+        unsafe {
+            LKRoomConnect(
+                self.native_room,
+                url.as_concrete_TypeRef(),
+                token.as_concrete_TypeRef(),
+                did_connect,
+                tx,
+            )
+        }
+
+        let this = self.clone();
+        let url = url.to_string();
+        let token = token.to_string();
+        async move {
+            rx.await.unwrap().context("error connecting to room")?;
+            *this.connection.lock().0.borrow_mut() = ConnectionState::Connected { url, token };
+            Ok(())
+        }
+    }
+
+    fn did_disconnect(&self) {
+        *self.connection.lock().0.borrow_mut() = ConnectionState::Disconnected;
+    }
+
+    pub fn display_sources(self: &Arc<Self>) -> impl Future<Output = Result<Vec<MacOSDisplay>>> {
+        extern "C" fn callback(tx: *mut c_void, sources: CFArrayRef, error: CFStringRef) {
+            unsafe {
+                let tx = Box::from_raw(tx as *mut oneshot::Sender<Result<Vec<MacOSDisplay>>>);
+
+                if sources.is_null() {
+                    let _ = tx.send(Err(anyhow!("{}", CFString::wrap_under_get_rule(error))));
+                } else {
+                    let sources = CFArray::wrap_under_get_rule(sources)
+                        .into_iter()
+                        .map(|source| MacOSDisplay::new(swift::MacOSDisplay(*source)))
+                        .collect();
+
+                    let _ = tx.send(Ok(sources));
+                }
+            }
+        }
+
+        let (tx, rx) = oneshot::channel();
+
+        unsafe {
+            LKDisplaySources(Box::into_raw(Box::new(tx)) as *mut _, callback);
+        }
+
+        async move { rx.await.unwrap() }
+    }
+
+    pub fn publish_video_track(
+        self: &Arc<Self>,
+        track: LocalVideoTrack,
+    ) -> impl Future<Output = Result<LocalTrackPublication>> {
+        let (tx, rx) = oneshot::channel::<Result<LocalTrackPublication>>();
+        extern "C" fn callback(
+            tx: *mut c_void,
+            publication: swift::LocalTrackPublication,
+            error: CFStringRef,
+        ) {
+            let tx =
+                unsafe { Box::from_raw(tx as *mut oneshot::Sender<Result<LocalTrackPublication>>) };
+            if error.is_null() {
+                let _ = tx.send(Ok(LocalTrackPublication::new(publication)));
+            } else {
+                let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
+                let _ = tx.send(Err(anyhow!(error)));
+            }
+        }
+        unsafe {
+            LKRoomPublishVideoTrack(
+                self.native_room,
+                track.0,
+                callback,
+                Box::into_raw(Box::new(tx)) as *mut c_void,
+            );
+        }
+        async { rx.await.unwrap().context("error publishing video track") }
+    }
+
+    pub fn publish_audio_track(
+        self: &Arc<Self>,
+        track: LocalAudioTrack,
+    ) -> impl Future<Output = Result<LocalTrackPublication>> {
+        let (tx, rx) = oneshot::channel::<Result<LocalTrackPublication>>();
+        extern "C" fn callback(
+            tx: *mut c_void,
+            publication: swift::LocalTrackPublication,
+            error: CFStringRef,
+        ) {
+            let tx =
+                unsafe { Box::from_raw(tx as *mut oneshot::Sender<Result<LocalTrackPublication>>) };
+            if error.is_null() {
+                let _ = tx.send(Ok(LocalTrackPublication::new(publication)));
+            } else {
+                let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
+                let _ = tx.send(Err(anyhow!(error)));
+            }
+        }
+        unsafe {
+            LKRoomPublishAudioTrack(
+                self.native_room,
+                track.0,
+                callback,
+                Box::into_raw(Box::new(tx)) as *mut c_void,
+            );
+        }
+        async { rx.await.unwrap().context("error publishing audio track") }
+    }
+
+    pub fn unpublish_track(&self, publication: LocalTrackPublication) {
+        unsafe {
+            LKRoomUnpublishTrack(self.native_room, publication.0);
+        }
+    }
+
+    pub fn remote_video_tracks(&self, participant_id: &str) -> Vec<Arc<RemoteVideoTrack>> {
+        unsafe {
+            let tracks = LKRoomVideoTracksForRemoteParticipant(
+                self.native_room,
+                CFString::new(participant_id).as_concrete_TypeRef(),
+            );
+
+            if tracks.is_null() {
+                Vec::new()
+            } else {
+                let tracks = CFArray::wrap_under_get_rule(tracks);
+                tracks
+                    .into_iter()
+                    .map(|native_track| {
+                        let native_track = swift::RemoteVideoTrack(*native_track);
+                        let id =
+                            CFString::wrap_under_get_rule(LKRemoteVideoTrackGetSid(native_track))
+                                .to_string();
+                        Arc::new(RemoteVideoTrack::new(
+                            native_track,
+                            id,
+                            participant_id.into(),
+                        ))
+                    })
+                    .collect()
+            }
+        }
+    }
+
+    pub fn remote_audio_tracks(&self, participant_id: &str) -> Vec<Arc<RemoteAudioTrack>> {
+        unsafe {
+            let tracks = LKRoomAudioTracksForRemoteParticipant(
+                self.native_room,
+                CFString::new(participant_id).as_concrete_TypeRef(),
+            );
+
+            if tracks.is_null() {
+                Vec::new()
+            } else {
+                let tracks = CFArray::wrap_under_get_rule(tracks);
+                tracks
+                    .into_iter()
+                    .map(|native_track| {
+                        let native_track = swift::RemoteAudioTrack(*native_track);
+                        let id =
+                            CFString::wrap_under_get_rule(LKRemoteAudioTrackGetSid(native_track))
+                                .to_string();
+                        Arc::new(RemoteAudioTrack::new(
+                            native_track,
+                            id,
+                            participant_id.into(),
+                        ))
+                    })
+                    .collect()
+            }
+        }
+    }
+
+    pub fn remote_audio_track_publications(
+        &self,
+        participant_id: &str,
+    ) -> Vec<Arc<RemoteTrackPublication>> {
+        unsafe {
+            let tracks = LKRoomAudioTrackPublicationsForRemoteParticipant(
+                self.native_room,
+                CFString::new(participant_id).as_concrete_TypeRef(),
+            );
+
+            if tracks.is_null() {
+                Vec::new()
+            } else {
+                let tracks = CFArray::wrap_under_get_rule(tracks);
+                tracks
+                    .into_iter()
+                    .map(|native_track_publication| {
+                        let native_track_publication =
+                            swift::RemoteTrackPublication(*native_track_publication);
+                        Arc::new(RemoteTrackPublication::new(native_track_publication))
+                    })
+                    .collect()
+            }
+        }
+    }
+
+    pub fn updates(&self) -> mpsc::UnboundedReceiver<RoomUpdate> {
+        let (tx, rx) = mpsc::unbounded();
+        self.update_subscribers.lock().push(tx);
+        rx
+    }
+
+    fn did_subscribe_to_remote_audio_track(
+        &self,
+        track: RemoteAudioTrack,
+        publication: RemoteTrackPublication,
+    ) {
+        let track = Arc::new(track);
+        let publication = Arc::new(publication);
+        self.update_subscribers.lock().retain(|tx| {
+            tx.unbounded_send(RoomUpdate::SubscribedToRemoteAudioTrack(
+                track.clone(),
+                publication.clone(),
+            ))
+            .is_ok()
+        });
+    }
+
+    fn did_unsubscribe_from_remote_audio_track(&self, publisher_id: String, track_id: String) {
+        self.update_subscribers.lock().retain(|tx| {
+            tx.unbounded_send(RoomUpdate::UnsubscribedFromRemoteAudioTrack {
+                publisher_id: publisher_id.clone(),
+                track_id: track_id.clone(),
+            })
+            .is_ok()
+        });
+    }
+
+    fn mute_changed_from_remote_audio_track(&self, track_id: String, muted: bool) {
+        self.update_subscribers.lock().retain(|tx| {
+            tx.unbounded_send(RoomUpdate::RemoteAudioTrackMuteChanged {
+                track_id: track_id.clone(),
+                muted,
+            })
+            .is_ok()
+        });
+    }
+
+    fn active_speakers_changed(&self, speakers: Vec<String>) {
+        self.update_subscribers.lock().retain(move |tx| {
+            tx.unbounded_send(RoomUpdate::ActiveSpeakersChanged {
+                speakers: speakers.clone(),
+            })
+            .is_ok()
+        });
+    }
+
+    fn did_subscribe_to_remote_video_track(&self, track: RemoteVideoTrack) {
+        let track = Arc::new(track);
+        self.update_subscribers.lock().retain(|tx| {
+            tx.unbounded_send(RoomUpdate::SubscribedToRemoteVideoTrack(track.clone()))
+                .is_ok()
+        });
+    }
+
+    fn did_unsubscribe_from_remote_video_track(&self, publisher_id: String, track_id: String) {
+        self.update_subscribers.lock().retain(|tx| {
+            tx.unbounded_send(RoomUpdate::UnsubscribedFromRemoteVideoTrack {
+                publisher_id: publisher_id.clone(),
+                track_id: track_id.clone(),
+            })
+            .is_ok()
+        });
+    }
+
+    fn build_done_callback() -> (
+        extern "C" fn(*mut c_void, CFStringRef),
+        *mut c_void,
+        oneshot::Receiver<Result<()>>,
+    ) {
+        let (tx, rx) = oneshot::channel();
+        extern "C" fn done_callback(tx: *mut c_void, error: CFStringRef) {
+            let tx = unsafe { Box::from_raw(tx as *mut oneshot::Sender<Result<()>>) };
+            if error.is_null() {
+                let _ = tx.send(Ok(()));
+            } else {
+                let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
+                let _ = tx.send(Err(anyhow!(error)));
+            }
+        }
+        (
+            done_callback,
+            Box::into_raw(Box::new(tx)) as *mut c_void,
+            rx,
+        )
+    }
+
+    pub fn set_display_sources(&self, _: Vec<MacOSDisplay>) {
+        unreachable!("This is a test-only function")
+    }
+}
+
+impl Drop for Room {
+    fn drop(&mut self) {
+        unsafe {
+            LKRoomDisconnect(self.native_room);
+            CFRelease(self.native_room.0);
+        }
+    }
+}
+
+struct RoomDelegate {
+    native_delegate: swift::RoomDelegate,
+    weak_room: *mut c_void,
+}
+
+impl RoomDelegate {
+    fn new(weak_room: Weak<Room>) -> Self {
+        let weak_room = weak_room.into_raw() as *mut c_void;
+        let native_delegate = unsafe {
+            LKRoomDelegateCreate(
+                weak_room,
+                Self::on_did_disconnect,
+                Self::on_did_subscribe_to_remote_audio_track,
+                Self::on_did_unsubscribe_from_remote_audio_track,
+                Self::on_mute_change_from_remote_audio_track,
+                Self::on_active_speakers_changed,
+                Self::on_did_subscribe_to_remote_video_track,
+                Self::on_did_unsubscribe_from_remote_video_track,
+                Self::on_did_publish_or_unpublish_local_audio_track,
+                Self::on_did_publish_or_unpublish_local_video_track,
+            )
+        };
+        Self {
+            native_delegate,
+            weak_room,
+        }
+    }
+
+    extern "C" fn on_did_disconnect(room: *mut c_void) {
+        let room = unsafe { Weak::from_raw(room as *mut Room) };
+        if let Some(room) = room.upgrade() {
+            room.did_disconnect();
+        }
+        let _ = Weak::into_raw(room);
+    }
+
+    extern "C" fn on_did_subscribe_to_remote_audio_track(
+        room: *mut c_void,
+        publisher_id: CFStringRef,
+        track_id: CFStringRef,
+        track: swift::RemoteAudioTrack,
+        publication: swift::RemoteTrackPublication,
+    ) {
+        let room = unsafe { Weak::from_raw(room as *mut Room) };
+        let publisher_id = unsafe { CFString::wrap_under_get_rule(publisher_id).to_string() };
+        let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() };
+        let track = RemoteAudioTrack::new(track, track_id, publisher_id);
+        let publication = RemoteTrackPublication::new(publication);
+        if let Some(room) = room.upgrade() {
+            room.did_subscribe_to_remote_audio_track(track, publication);
+        }
+        let _ = Weak::into_raw(room);
+    }
+
+    extern "C" fn on_did_unsubscribe_from_remote_audio_track(
+        room: *mut c_void,
+        publisher_id: CFStringRef,
+        track_id: CFStringRef,
+    ) {
+        let room = unsafe { Weak::from_raw(room as *mut Room) };
+        let publisher_id = unsafe { CFString::wrap_under_get_rule(publisher_id).to_string() };
+        let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() };
+        if let Some(room) = room.upgrade() {
+            room.did_unsubscribe_from_remote_audio_track(publisher_id, track_id);
+        }
+        let _ = Weak::into_raw(room);
+    }
+
+    extern "C" fn on_mute_change_from_remote_audio_track(
+        room: *mut c_void,
+        track_id: CFStringRef,
+        muted: bool,
+    ) {
+        let room = unsafe { Weak::from_raw(room as *mut Room) };
+        let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() };
+        if let Some(room) = room.upgrade() {
+            room.mute_changed_from_remote_audio_track(track_id, muted);
+        }
+        let _ = Weak::into_raw(room);
+    }
+
+    extern "C" fn on_active_speakers_changed(room: *mut c_void, participants: CFArrayRef) {
+        if participants.is_null() {
+            return;
+        }
+
+        let room = unsafe { Weak::from_raw(room as *mut Room) };
+        let speakers = unsafe {
+            CFArray::wrap_under_get_rule(participants)
+                .into_iter()
+                .map(
+                    |speaker: core_foundation::base::ItemRef<'_, *const c_void>| {
+                        CFString::wrap_under_get_rule(*speaker as CFStringRef).to_string()
+                    },
+                )
+                .collect()
+        };
+
+        if let Some(room) = room.upgrade() {
+            room.active_speakers_changed(speakers);
+        }
+        let _ = Weak::into_raw(room);
+    }
+
+    extern "C" fn on_did_subscribe_to_remote_video_track(
+        room: *mut c_void,
+        publisher_id: CFStringRef,
+        track_id: CFStringRef,
+        track: swift::RemoteVideoTrack,
+    ) {
+        let room = unsafe { Weak::from_raw(room as *mut Room) };
+        let publisher_id = unsafe { CFString::wrap_under_get_rule(publisher_id).to_string() };
+        let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() };
+        let track = RemoteVideoTrack::new(track, track_id, publisher_id);
+        if let Some(room) = room.upgrade() {
+            room.did_subscribe_to_remote_video_track(track);
+        }
+        let _ = Weak::into_raw(room);
+    }
+
+    extern "C" fn on_did_unsubscribe_from_remote_video_track(
+        room: *mut c_void,
+        publisher_id: CFStringRef,
+        track_id: CFStringRef,
+    ) {
+        let room = unsafe { Weak::from_raw(room as *mut Room) };
+        let publisher_id = unsafe { CFString::wrap_under_get_rule(publisher_id).to_string() };
+        let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() };
+        if let Some(room) = room.upgrade() {
+            room.did_unsubscribe_from_remote_video_track(publisher_id, track_id);
+        }
+        let _ = Weak::into_raw(room);
+    }
+
+    extern "C" fn on_did_publish_or_unpublish_local_audio_track(
+        room: *mut c_void,
+        publication: swift::LocalTrackPublication,
+        is_published: bool,
+    ) {
+        let room = unsafe { Weak::from_raw(room as *mut Room) };
+        if let Some(room) = room.upgrade() {
+            let publication = LocalTrackPublication::new(publication);
+            let update = if is_published {
+                RoomUpdate::LocalAudioTrackPublished { publication }
+            } else {
+                RoomUpdate::LocalAudioTrackUnpublished { publication }
+            };
+            room.update_subscribers
+                .lock()
+                .retain(|tx| tx.unbounded_send(update.clone()).is_ok());
+        }
+        let _ = Weak::into_raw(room);
+    }
+
+    extern "C" fn on_did_publish_or_unpublish_local_video_track(
+        room: *mut c_void,
+        publication: swift::LocalTrackPublication,
+        is_published: bool,
+    ) {
+        let room = unsafe { Weak::from_raw(room as *mut Room) };
+        if let Some(room) = room.upgrade() {
+            let publication = LocalTrackPublication::new(publication);
+            let update = if is_published {
+                RoomUpdate::LocalVideoTrackPublished { publication }
+            } else {
+                RoomUpdate::LocalVideoTrackUnpublished { publication }
+            };
+            room.update_subscribers
+                .lock()
+                .retain(|tx| tx.unbounded_send(update.clone()).is_ok());
+        }
+        let _ = Weak::into_raw(room);
+    }
+}
+
+impl Drop for RoomDelegate {
+    fn drop(&mut self) {
+        unsafe {
+            CFRelease(self.native_delegate.0);
+            let _ = Weak::from_raw(self.weak_room as *mut Room);
+        }
+    }
+}
+
+pub struct LocalAudioTrack(swift::LocalAudioTrack);
+
+impl LocalAudioTrack {
+    pub fn create() -> Self {
+        Self(unsafe { LKLocalAudioTrackCreateTrack() })
+    }
+}
+
+impl Drop for LocalAudioTrack {
+    fn drop(&mut self) {
+        unsafe { CFRelease(self.0 .0) }
+    }
+}
+
+pub struct LocalVideoTrack(swift::LocalVideoTrack);
+
+impl LocalVideoTrack {
+    pub fn screen_share_for_display(display: &MacOSDisplay) -> Self {
+        Self(unsafe { LKCreateScreenShareTrackForDisplay(display.0) })
+    }
+}
+
+impl Drop for LocalVideoTrack {
+    fn drop(&mut self) {
+        unsafe { CFRelease(self.0 .0) }
+    }
+}
+
+pub struct LocalTrackPublication(swift::LocalTrackPublication);
+
+impl LocalTrackPublication {
+    pub fn new(native_track_publication: swift::LocalTrackPublication) -> Self {
+        unsafe {
+            CFRetain(native_track_publication.0);
+        }
+        Self(native_track_publication)
+    }
+
+    pub fn sid(&self) -> String {
+        unsafe { CFString::wrap_under_get_rule(LKLocalTrackPublicationGetSid(self.0)).to_string() }
+    }
+
+    pub fn set_mute(&self, muted: bool) -> impl Future<Output = Result<()>> {
+        let (tx, rx) = futures::channel::oneshot::channel();
+
+        extern "C" fn complete_callback(callback_data: *mut c_void, error: CFStringRef) {
+            let tx = unsafe { Box::from_raw(callback_data as *mut oneshot::Sender<Result<()>>) };
+            if error.is_null() {
+                tx.send(Ok(())).ok();
+            } else {
+                let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
+                tx.send(Err(anyhow!(error))).ok();
+            }
+        }
+
+        unsafe {
+            LKLocalTrackPublicationSetMute(
+                self.0,
+                muted,
+                complete_callback,
+                Box::into_raw(Box::new(tx)) as *mut c_void,
+            )
+        }
+
+        async move { rx.await.unwrap() }
+    }
+
+    pub fn is_muted(&self) -> bool {
+        unsafe { LKLocalTrackPublicationIsMuted(self.0) }
+    }
+}
+
+impl Clone for LocalTrackPublication {
+    fn clone(&self) -> Self {
+        unsafe {
+            CFRetain(self.0 .0);
+        }
+        Self(self.0)
+    }
+}
+
+impl Drop for LocalTrackPublication {
+    fn drop(&mut self) {
+        unsafe { CFRelease(self.0 .0) }
+    }
+}
+
+pub struct RemoteTrackPublication(swift::RemoteTrackPublication);
+
+impl RemoteTrackPublication {
+    pub fn new(native_track_publication: swift::RemoteTrackPublication) -> Self {
+        unsafe {
+            CFRetain(native_track_publication.0);
+        }
+        Self(native_track_publication)
+    }
+
+    pub fn sid(&self) -> String {
+        unsafe { CFString::wrap_under_get_rule(LKRemoteTrackPublicationGetSid(self.0)).to_string() }
+    }
+
+    pub fn is_muted(&self) -> bool {
+        unsafe { LKRemoteTrackPublicationIsMuted(self.0) }
+    }
+
+    pub fn set_enabled(&self, enabled: bool) -> impl Future<Output = Result<()>> {
+        let (tx, rx) = futures::channel::oneshot::channel();
+
+        extern "C" fn complete_callback(callback_data: *mut c_void, error: CFStringRef) {
+            let tx = unsafe { Box::from_raw(callback_data as *mut oneshot::Sender<Result<()>>) };
+            if error.is_null() {
+                tx.send(Ok(())).ok();
+            } else {
+                let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
+                tx.send(Err(anyhow!(error))).ok();
+            }
+        }
+
+        unsafe {
+            LKRemoteTrackPublicationSetEnabled(
+                self.0,
+                enabled,
+                complete_callback,
+                Box::into_raw(Box::new(tx)) as *mut c_void,
+            )
+        }
+
+        async move { rx.await.unwrap() }
+    }
+}
+
+impl Drop for RemoteTrackPublication {
+    fn drop(&mut self) {
+        unsafe { CFRelease(self.0 .0) }
+    }
+}
+
+#[derive(Debug)]
+pub struct RemoteAudioTrack {
+    native_track: swift::RemoteAudioTrack,
+    sid: Sid,
+    publisher_id: String,
+}
+
+impl RemoteAudioTrack {
+    fn new(native_track: swift::RemoteAudioTrack, sid: Sid, publisher_id: String) -> Self {
+        unsafe {
+            CFRetain(native_track.0);
+        }
+        Self {
+            native_track,
+            sid,
+            publisher_id,
+        }
+    }
+
+    pub fn sid(&self) -> &str {
+        &self.sid
+    }
+
+    pub fn publisher_id(&self) -> &str {
+        &self.publisher_id
+    }
+
+    pub fn start(&self) {
+        unsafe { LKRemoteAudioTrackStart(self.native_track) }
+    }
+
+    pub fn stop(&self) {
+        unsafe { LKRemoteAudioTrackStop(self.native_track) }
+    }
+}
+
+impl Drop for RemoteAudioTrack {
+    fn drop(&mut self) {
+        // todo: uncomment this `CFRelease`, unless we find that it was causing
+        // the crash in the `livekit.multicast` thread.
+        //
+        // unsafe { CFRelease(self.native_track.0) }
+        let _ = self.native_track;
+    }
+}
+
+#[derive(Debug)]
+pub struct RemoteVideoTrack {
+    native_track: swift::RemoteVideoTrack,
+    sid: Sid,
+    publisher_id: String,
+}
+
+impl RemoteVideoTrack {
+    fn new(native_track: swift::RemoteVideoTrack, sid: Sid, publisher_id: String) -> Self {
+        unsafe {
+            CFRetain(native_track.0);
+        }
+        Self {
+            native_track,
+            sid,
+            publisher_id,
+        }
+    }
+
+    pub fn sid(&self) -> &str {
+        &self.sid
+    }
+
+    pub fn publisher_id(&self) -> &str {
+        &self.publisher_id
+    }
+
+    pub fn frames(&self) -> async_broadcast::Receiver<Frame> {
+        extern "C" fn on_frame(callback_data: *mut c_void, frame: CVImageBufferRef) -> bool {
+            unsafe {
+                let tx = Box::from_raw(callback_data as *mut async_broadcast::Sender<Frame>);
+                let buffer = CVImageBuffer::wrap_under_get_rule(frame);
+                let result = tx.try_broadcast(Frame(buffer));
+                let _ = Box::into_raw(tx);
+                match result {
+                    Ok(_) => true,
+                    Err(async_broadcast::TrySendError::Closed(_))
+                    | Err(async_broadcast::TrySendError::Inactive(_)) => {
+                        log::warn!("no active receiver for frame");
+                        false
+                    }
+                    Err(async_broadcast::TrySendError::Full(_)) => {
+                        log::warn!("skipping frame as receiver is not keeping up");
+                        true
+                    }
+                }
+            }
+        }
+
+        extern "C" fn on_drop(callback_data: *mut c_void) {
+            unsafe {
+                let _ = Box::from_raw(callback_data as *mut async_broadcast::Sender<Frame>);
+            }
+        }
+
+        let (tx, rx) = async_broadcast::broadcast(64);
+        unsafe {
+            let renderer = LKVideoRendererCreate(
+                Box::into_raw(Box::new(tx)) as *mut c_void,
+                on_frame,
+                on_drop,
+            );
+            LKVideoTrackAddRenderer(self.native_track, renderer);
+            rx
+        }
+    }
+}
+
+impl Drop for RemoteVideoTrack {
+    fn drop(&mut self) {
+        unsafe { CFRelease(self.native_track.0) }
+    }
+}
+
+pub struct MacOSDisplay(swift::MacOSDisplay);
+
+impl MacOSDisplay {
+    fn new(ptr: swift::MacOSDisplay) -> Self {
+        unsafe {
+            CFRetain(ptr.0);
+        }
+        Self(ptr)
+    }
+}
+
+impl Drop for MacOSDisplay {
+    fn drop(&mut self) {
+        unsafe { CFRelease(self.0 .0) }
+    }
+}
+
+#[derive(Clone)]
+pub struct Frame(CVImageBuffer);
+
+impl Frame {
+    pub fn width(&self) -> usize {
+        self.0.width()
+    }
+
+    pub fn height(&self) -> usize {
+        self.0.height()
+    }
+
+    pub fn image(&self) -> CVImageBuffer {
+        self.0.clone()
+    }
+}

crates/live_kit_client/src/remote_video_track_view.rs 🔗

@@ -1,61 +0,0 @@
-use crate::track::RemoteVideoTrack;
-use anyhow::Result;
-use futures::StreamExt as _;
-use gpui::{
-    Empty, EventEmitter, IntoElement, Render, ScreenCaptureFrame, Task, View, ViewContext,
-    VisualContext as _,
-};
-
-pub struct RemoteVideoTrackView {
-    track: RemoteVideoTrack,
-    frame: Option<ScreenCaptureFrame>,
-    _maintain_frame: Task<Result<()>>,
-}
-
-#[derive(Debug)]
-pub enum RemoteVideoTrackViewEvent {
-    Close,
-}
-
-impl RemoteVideoTrackView {
-    pub fn new(track: RemoteVideoTrack, cx: &mut ViewContext<Self>) -> Self {
-        cx.focus_handle();
-        let frames = super::play_remote_video_track(&track);
-
-        Self {
-            track,
-            frame: None,
-            _maintain_frame: cx.spawn(|this, mut cx| async move {
-                futures::pin_mut!(frames);
-                while let Some(frame) = frames.next().await {
-                    this.update(&mut cx, |this, cx| {
-                        this.frame = Some(frame);
-                        cx.notify();
-                    })?;
-                }
-                this.update(&mut cx, |_, cx| cx.emit(RemoteVideoTrackViewEvent::Close))?;
-                Ok(())
-            }),
-        }
-    }
-
-    pub fn clone(&self, cx: &mut ViewContext<Self>) -> View<Self> {
-        cx.new_view(|cx| Self::new(self.track.clone(), cx))
-    }
-}
-
-impl EventEmitter<RemoteVideoTrackViewEvent> for RemoteVideoTrackView {}
-
-impl Render for RemoteVideoTrackView {
-    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        #[cfg(target_os = "macos")]
-        if let Some(frame) = &self.frame {
-            use gpui::Styled as _;
-            return gpui::surface(frame.0.clone())
-                .size_full()
-                .into_any_element();
-        }
-
-        Empty.into_any_element()
-    }
-}

crates/live_kit_client/src/test.rs 🔗

@@ -1,42 +1,32 @@
-pub mod participant;
-pub mod publication;
-pub mod track;
-
-#[cfg(not(windows))]
-pub mod webrtc;
-
-#[cfg(not(windows))]
-use self::id::*;
-use self::{participant::*, publication::*, track::*};
+use crate::{ConnectionState, RoomUpdate, Sid};
 use anyhow::{anyhow, Context, Result};
 use async_trait::async_trait;
 use collections::{btree_map::Entry as BTreeEntry, hash_map::Entry, BTreeMap, HashMap, HashSet};
-use gpui::BackgroundExecutor;
+use futures::Stream;
+use gpui::{BackgroundExecutor, SurfaceSource};
 use live_kit_server::{proto, token};
-#[cfg(not(windows))]
-use livekit::options::TrackPublishOptions;
+
 use parking_lot::Mutex;
-use postage::{mpsc, sink::Sink};
-use std::sync::{
-    atomic::{AtomicBool, Ordering::SeqCst},
-    Arc, Weak,
+use postage::watch;
+use std::{
+    future::Future,
+    mem,
+    sync::{
+        atomic::{AtomicBool, Ordering::SeqCst},
+        Arc, Weak,
+    },
 };
 
-#[cfg(not(windows))]
-pub use livekit::{id, options, ConnectionState, DisconnectReason, RoomOptions};
-
 static SERVERS: Mutex<BTreeMap<String, Arc<TestServer>>> = Mutex::new(BTreeMap::new());
 
 pub struct TestServer {
     pub url: String,
     pub api_key: String,
     pub secret_key: String,
-    #[cfg(not(target_os = "windows"))]
     rooms: Mutex<HashMap<String, TestServerRoom>>,
     executor: BackgroundExecutor,
 }
 
-#[cfg(not(target_os = "windows"))]
 impl TestServer {
     pub fn create(
         url: String,
@@ -83,8 +73,9 @@ impl TestServer {
     }
 
     pub async fn create_room(&self, room: String) -> Result<()> {
+        // todo(linux): Remove this once the cross-platform LiveKit implementation is merged
+        #[cfg(any(test, feature = "test-support"))]
         self.executor.simulate_random_delay().await;
-
         let mut server_rooms = self.rooms.lock();
         if let Entry::Vacant(e) = server_rooms.entry(room.clone()) {
             e.insert(Default::default());
@@ -95,8 +86,10 @@ impl TestServer {
     }
 
     async fn delete_room(&self, room: String) -> Result<()> {
+        // TODO: clear state associated with all `Room`s.
+        // todo(linux): Remove this once the cross-platform LiveKit implementation is merged
+        #[cfg(any(test, feature = "test-support"))]
         self.executor.simulate_random_delay().await;
-
         let mut server_rooms = self.rooms.lock();
         server_rooms
             .remove(&room)
@@ -104,64 +97,46 @@ impl TestServer {
         Ok(())
     }
 
-    async fn join_room(&self, token: String, client_room: Room) -> Result<ParticipantIdentity> {
+    async fn join_room(&self, token: String, client_room: Arc<Room>) -> Result<()> {
+        // todo(linux): Remove this once the cross-platform LiveKit implementation is merged
+        #[cfg(any(test, feature = "test-support"))]
         self.executor.simulate_random_delay().await;
 
         let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
-        let identity = ParticipantIdentity(claims.sub.unwrap().to_string());
+        let identity = claims.sub.unwrap().to_string();
         let room_name = claims.video.room.unwrap();
         let mut server_rooms = self.rooms.lock();
         let room = (*server_rooms).entry(room_name.to_string()).or_default();
 
         if let Entry::Vacant(e) = room.client_rooms.entry(identity.clone()) {
-            for server_track in &room.video_tracks {
-                let track = RemoteTrack::Video(RemoteVideoTrack {
-                    server_track: server_track.clone(),
-                    _room: client_room.downgrade(),
-                });
+            for track in &room.video_tracks {
                 client_room
                     .0
                     .lock()
                     .updates_tx
-                    .blocking_send(RoomEvent::TrackSubscribed {
-                        track: track.clone(),
-                        publication: RemoteTrackPublication {
-                            sid: server_track.sid.clone(),
-                            room: client_room.downgrade(),
-                            track,
-                        },
-                        participant: RemoteParticipant {
-                            room: client_room.downgrade(),
-                            identity: server_track.publisher_id.clone(),
+                    .try_broadcast(RoomUpdate::SubscribedToRemoteVideoTrack(Arc::new(
+                        RemoteVideoTrack {
+                            server_track: track.clone(),
                         },
-                    })
+                    )))
                     .unwrap();
             }
-            for server_track in &room.audio_tracks {
-                let track = RemoteTrack::Audio(RemoteAudioTrack {
-                    server_track: server_track.clone(),
-                    room: client_room.downgrade(),
-                });
+            for track in &room.audio_tracks {
                 client_room
                     .0
                     .lock()
                     .updates_tx
-                    .blocking_send(RoomEvent::TrackSubscribed {
-                        track: track.clone(),
-                        publication: RemoteTrackPublication {
-                            sid: server_track.sid.clone(),
-                            room: client_room.downgrade(),
-                            track,
-                        },
-                        participant: RemoteParticipant {
-                            room: client_room.downgrade(),
-                            identity: server_track.publisher_id.clone(),
-                        },
-                    })
+                    .try_broadcast(RoomUpdate::SubscribedToRemoteAudioTrack(
+                        Arc::new(RemoteAudioTrack {
+                            server_track: track.clone(),
+                            room: Arc::downgrade(&client_room),
+                        }),
+                        Arc::new(RemoteTrackPublication),
+                    ))
                     .unwrap();
             }
             e.insert(client_room);
-            Ok(identity)
+            Ok(())
         } else {
             Err(anyhow!(
                 "{:?} attempted to join room {:?} twice",
@@ -172,10 +147,11 @@ impl TestServer {
     }
 
     async fn leave_room(&self, token: String) -> Result<()> {
+        // todo(linux): Remove this once the cross-platform LiveKit implementation is merged
+        #[cfg(any(test, feature = "test-support"))]
         self.executor.simulate_random_delay().await;
-
         let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
-        let identity = ParticipantIdentity(claims.sub.unwrap().to_string());
+        let identity = claims.sub.unwrap().to_string();
         let room_name = claims.video.room.unwrap();
         let mut server_rooms = self.rooms.lock();
         let room = server_rooms
@@ -191,44 +167,10 @@ impl TestServer {
         Ok(())
     }
 
-    fn remote_participants(
-        &self,
-        token: String,
-    ) -> Result<HashMap<ParticipantIdentity, RemoteParticipant>> {
-        let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
-        let local_identity = ParticipantIdentity(claims.sub.unwrap().to_string());
-        let room_name = claims.video.room.unwrap().to_string();
-
-        if let Some(server_room) = self.rooms.lock().get(&room_name) {
-            let room = server_room
-                .client_rooms
-                .get(&local_identity)
-                .unwrap()
-                .downgrade();
-            Ok(server_room
-                .client_rooms
-                .iter()
-                .filter(|(identity, _)| *identity != &local_identity)
-                .map(|(identity, _)| {
-                    (
-                        identity.clone(),
-                        RemoteParticipant {
-                            room: room.clone(),
-                            identity: identity.clone(),
-                        },
-                    )
-                })
-                .collect())
-        } else {
-            Ok(Default::default())
-        }
-    }
-
-    async fn remove_participant(
-        &self,
-        room_name: String,
-        identity: ParticipantIdentity,
-    ) -> Result<()> {
+    async fn remove_participant(&self, room_name: String, identity: String) -> Result<()> {
+        // TODO: clear state associated with the `Room`.
+        // todo(linux): Remove this once the cross-platform LiveKit implementation is merged
+        #[cfg(any(test, feature = "test-support"))]
         self.executor.simulate_random_delay().await;
 
         let mut server_rooms = self.rooms.lock();
@@ -251,32 +193,25 @@ impl TestServer {
         identity: String,
         permission: proto::ParticipantPermission,
     ) -> Result<()> {
+        // todo(linux): Remove this once the cross-platform LiveKit implementation is merged
+        #[cfg(any(test, feature = "test-support"))]
         self.executor.simulate_random_delay().await;
-
         let mut server_rooms = self.rooms.lock();
         let room = server_rooms
             .get_mut(&room_name)
             .ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
-        room.participant_permissions
-            .insert(ParticipantIdentity(identity), permission);
+        room.participant_permissions.insert(identity, permission);
         Ok(())
     }
 
     pub async fn disconnect_client(&self, client_identity: String) {
-        let client_identity = ParticipantIdentity(client_identity);
-
+        // todo(linux): Remove this once the cross-platform LiveKit implementation is merged
+        #[cfg(any(test, feature = "test-support"))]
         self.executor.simulate_random_delay().await;
-
         let mut server_rooms = self.rooms.lock();
         for room in server_rooms.values_mut() {
             if let Some(room) = room.client_rooms.remove(&client_identity) {
-                let mut room = room.0.lock();
-                room.connection_state = ConnectionState::Disconnected;
-                room.updates_tx
-                    .blocking_send(RoomEvent::Disconnected {
-                        reason: DisconnectReason::SignalClose,
-                    })
-                    .ok();
+                *room.0.lock().connection.0.borrow_mut() = ConnectionState::Disconnected;
             }
         }
     }
@@ -284,12 +219,13 @@ impl TestServer {
     async fn publish_video_track(
         &self,
         token: String,
-        _local_track: LocalVideoTrack,
-    ) -> Result<TrackSid> {
+        local_track: LocalVideoTrack,
+    ) -> Result<Sid> {
+        // todo(linux): Remove this once the cross-platform LiveKit implementation is merged
+        #[cfg(any(test, feature = "test-support"))]
         self.executor.simulate_random_delay().await;
-
         let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
-        let identity = ParticipantIdentity(claims.sub.unwrap().to_string());
+        let identity = claims.sub.unwrap().to_string();
         let room_name = claims.video.room.unwrap();
 
         let mut server_rooms = self.rooms.lock();
@@ -308,38 +244,26 @@ impl TestServer {
             return Err(anyhow!("user is not allowed to publish"));
         }
 
-        let sid: TrackSid = format!("TR_{}", nanoid::nanoid!(17)).try_into().unwrap();
-        let server_track = Arc::new(TestServerVideoTrack {
+        let sid = nanoid::nanoid!(17);
+        let track = Arc::new(TestServerVideoTrack {
             sid: sid.clone(),
             publisher_id: identity.clone(),
+            frames_rx: local_track.frames_rx.clone(),
         });
 
-        room.video_tracks.push(server_track.clone());
-
-        for (room_identity, client_room) in &room.client_rooms {
-            if *room_identity != identity {
-                let track = RemoteTrack::Video(RemoteVideoTrack {
-                    server_track: server_track.clone(),
-                    _room: client_room.downgrade(),
-                });
-                let publication = RemoteTrackPublication {
-                    sid: sid.clone(),
-                    room: client_room.downgrade(),
-                    track: track.clone(),
-                };
-                let participant = RemoteParticipant {
-                    identity: identity.clone(),
-                    room: client_room.downgrade(),
-                };
-                client_room
+        room.video_tracks.push(track.clone());
+
+        for (id, client_room) in &room.client_rooms {
+            if *id != identity {
+                let _ = client_room
                     .0
                     .lock()
                     .updates_tx
-                    .blocking_send(RoomEvent::TrackSubscribed {
-                        track,
-                        publication,
-                        participant,
-                    })
+                    .try_broadcast(RoomUpdate::SubscribedToRemoteVideoTrack(Arc::new(
+                        RemoteVideoTrack {
+                            server_track: track.clone(),
+                        },
+                    )))
                     .unwrap();
             }
         }
@@ -351,11 +275,13 @@ impl TestServer {
         &self,
         token: String,
         _local_track: &LocalAudioTrack,
-    ) -> Result<TrackSid> {
+    ) -> Result<Sid> {
+        // todo(linux): Remove this once the cross-platform LiveKit implementation is merged
+        #[cfg(any(test, feature = "test-support"))]
         self.executor.simulate_random_delay().await;
 
         let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
-        let identity = ParticipantIdentity(claims.sub.unwrap().to_string());
+        let identity = claims.sub.unwrap().to_string();
         let room_name = claims.video.room.unwrap();
 
         let mut server_rooms = self.rooms.lock();
@@ -374,54 +300,41 @@ impl TestServer {
             return Err(anyhow!("user is not allowed to publish"));
         }
 
-        let sid: TrackSid = format!("TR_{}", nanoid::nanoid!(17)).try_into().unwrap();
-        let server_track = Arc::new(TestServerAudioTrack {
+        let sid = nanoid::nanoid!(17);
+        let track = Arc::new(TestServerAudioTrack {
             sid: sid.clone(),
             publisher_id: identity.clone(),
             muted: AtomicBool::new(false),
         });
 
-        room.audio_tracks.push(server_track.clone());
-
-        for (room_identity, client_room) in &room.client_rooms {
-            if *room_identity != identity {
-                let track = RemoteTrack::Audio(RemoteAudioTrack {
-                    server_track: server_track.clone(),
-                    room: client_room.downgrade(),
-                });
-                let publication = RemoteTrackPublication {
-                    sid: sid.clone(),
-                    room: client_room.downgrade(),
-                    track: track.clone(),
-                };
-                let participant = RemoteParticipant {
-                    identity: identity.clone(),
-                    room: client_room.downgrade(),
-                };
-                client_room
+        let publication = Arc::new(RemoteTrackPublication);
+
+        room.audio_tracks.push(track.clone());
+
+        for (id, client_room) in &room.client_rooms {
+            if *id != identity {
+                let _ = client_room
                     .0
                     .lock()
                     .updates_tx
-                    .blocking_send(RoomEvent::TrackSubscribed {
-                        track,
-                        publication,
-                        participant,
-                    })
-                    .ok();
+                    .try_broadcast(RoomUpdate::SubscribedToRemoteAudioTrack(
+                        Arc::new(RemoteAudioTrack {
+                            server_track: track.clone(),
+                            room: Arc::downgrade(client_room),
+                        }),
+                        publication.clone(),
+                    ))
+                    .unwrap();
             }
         }
 
         Ok(sid)
     }
 
-    async fn unpublish_track(&self, _token: String, _track: &TrackSid) -> Result<()> {
-        Ok(())
-    }
-
-    fn set_track_muted(&self, token: &str, track_sid: &TrackSid, muted: bool) -> Result<()> {
-        let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
+    fn set_track_muted(&self, token: &str, track_sid: &str, muted: bool) -> Result<()> {
+        let claims = live_kit_server::token::validate(token, &self.secret_key)?;
         let room_name = claims.video.room.unwrap();
-        let identity = ParticipantIdentity(claims.sub.unwrap().to_string());
+        let identity = claims.sub.unwrap();
         let mut server_rooms = self.rooms.lock();
         let room = server_rooms
             .get_mut(&*room_name)
@@ -429,42 +342,19 @@ impl TestServer {
         if let Some(track) = room
             .audio_tracks
             .iter_mut()
-            .find(|track| track.sid == *track_sid)
+            .find(|track| track.sid == track_sid)
         {
             track.muted.store(muted, SeqCst);
             for (id, client_room) in room.client_rooms.iter() {
                 if *id != identity {
-                    let participant = Participant::Remote(RemoteParticipant {
-                        identity: identity.clone(),
-                        room: client_room.downgrade(),
-                    });
-                    let track = RemoteTrack::Audio(RemoteAudioTrack {
-                        server_track: track.clone(),
-                        room: client_room.downgrade(),
-                    });
-                    let publication = TrackPublication::Remote(RemoteTrackPublication {
-                        sid: track_sid.clone(),
-                        room: client_room.downgrade(),
-                        track,
-                    });
-
-                    let event = if muted {
-                        RoomEvent::TrackMuted {
-                            participant,
-                            publication,
-                        }
-                    } else {
-                        RoomEvent::TrackUnmuted {
-                            participant,
-                            publication,
-                        }
-                    };
-
                     client_room
                         .0
                         .lock()
                         .updates_tx
-                        .blocking_send(event)
+                        .try_broadcast(RoomUpdate::RemoteAudioTrackMuteChanged {
+                            track_id: track_sid.to_string(),
+                            muted,
+                        })
                         .unwrap();
                 }
             }
@@ -472,14 +362,14 @@ impl TestServer {
         Ok(())
     }
 
-    fn is_track_muted(&self, token: &str, track_sid: &TrackSid) -> Option<bool> {
-        let claims = live_kit_server::token::validate(&token, &self.secret_key).ok()?;
+    fn is_track_muted(&self, token: &str, track_sid: &str) -> Option<bool> {
+        let claims = live_kit_server::token::validate(token, &self.secret_key).ok()?;
         let room_name = claims.video.room.unwrap();
 
         let mut server_rooms = self.rooms.lock();
         let room = server_rooms.get_mut(&*room_name)?;
         room.audio_tracks.iter().find_map(|track| {
-            if track.sid == *track_sid {
+            if track.sid == track_sid {
                 Some(track.muted.load(SeqCst))
             } else {
                 None
@@ -487,33 +377,33 @@ impl TestServer {
         })
     }
 
-    fn video_tracks(&self, token: String) -> Result<Vec<RemoteVideoTrack>> {
+    fn video_tracks(&self, token: String) -> Result<Vec<Arc<RemoteVideoTrack>>> {
         let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
         let room_name = claims.video.room.unwrap();
-        let identity = ParticipantIdentity(claims.sub.unwrap().to_string());
+        let identity = claims.sub.unwrap();
 
         let mut server_rooms = self.rooms.lock();
         let room = server_rooms
             .get_mut(&*room_name)
             .ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
-        let client_room = room
-            .client_rooms
-            .get(&identity)
+        room.client_rooms
+            .get(identity.as_ref())
             .ok_or_else(|| anyhow!("not a participant in room"))?;
         Ok(room
             .video_tracks
             .iter()
-            .map(|track| RemoteVideoTrack {
-                server_track: track.clone(),
-                _room: client_room.downgrade(),
+            .map(|track| {
+                Arc::new(RemoteVideoTrack {
+                    server_track: track.clone(),
+                })
             })
             .collect())
     }
 
-    fn audio_tracks(&self, token: String) -> Result<Vec<RemoteAudioTrack>> {
+    fn audio_tracks(&self, token: String) -> Result<Vec<Arc<RemoteAudioTrack>>> {
         let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
         let room_name = claims.video.room.unwrap();
-        let identity = ParticipantIdentity(claims.sub.unwrap().to_string());
+        let identity = claims.sub.unwrap();
 
         let mut server_rooms = self.rooms.lock();
         let room = server_rooms
@@ -521,125 +411,49 @@ impl TestServer {
             .ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
         let client_room = room
             .client_rooms
-            .get(&identity)
+            .get(identity.as_ref())
             .ok_or_else(|| anyhow!("not a participant in room"))?;
         Ok(room
             .audio_tracks
             .iter()
-            .map(|track| RemoteAudioTrack {
-                server_track: track.clone(),
-                room: client_room.downgrade(),
+            .map(|track| {
+                Arc::new(RemoteAudioTrack {
+                    server_track: track.clone(),
+                    room: Arc::downgrade(client_room),
+                })
             })
             .collect())
     }
 }
 
-#[cfg(not(target_os = "windows"))]
-#[derive(Default, Debug)]
+#[derive(Default)]
 struct TestServerRoom {
-    client_rooms: HashMap<ParticipantIdentity, Room>,
+    client_rooms: HashMap<Sid, Arc<Room>>,
     video_tracks: Vec<Arc<TestServerVideoTrack>>,
     audio_tracks: Vec<Arc<TestServerAudioTrack>>,
-    participant_permissions: HashMap<ParticipantIdentity, proto::ParticipantPermission>,
+    participant_permissions: HashMap<Sid, proto::ParticipantPermission>,
 }
 
-#[cfg(not(target_os = "windows"))]
 #[derive(Debug)]
 struct TestServerVideoTrack {
-    sid: TrackSid,
-    publisher_id: ParticipantIdentity,
-    // frames_rx: async_broadcast::Receiver<Frame>,
+    sid: Sid,
+    publisher_id: Sid,
+    frames_rx: async_broadcast::Receiver<Frame>,
 }
 
-#[cfg(not(target_os = "windows"))]
 #[derive(Debug)]
 struct TestServerAudioTrack {
-    sid: TrackSid,
-    publisher_id: ParticipantIdentity,
+    sid: Sid,
+    publisher_id: Sid,
     muted: AtomicBool,
 }
 
+impl TestServerRoom {}
+
 pub struct TestApiClient {
     url: String,
 }
 
-#[derive(Clone, Debug)]
-#[non_exhaustive]
-pub enum RoomEvent {
-    ParticipantConnected(RemoteParticipant),
-    ParticipantDisconnected(RemoteParticipant),
-    LocalTrackPublished {
-        publication: LocalTrackPublication,
-        track: LocalTrack,
-        participant: LocalParticipant,
-    },
-    LocalTrackUnpublished {
-        publication: LocalTrackPublication,
-        participant: LocalParticipant,
-    },
-    TrackSubscribed {
-        track: RemoteTrack,
-        publication: RemoteTrackPublication,
-        participant: RemoteParticipant,
-    },
-    TrackUnsubscribed {
-        track: RemoteTrack,
-        publication: RemoteTrackPublication,
-        participant: RemoteParticipant,
-    },
-    TrackSubscriptionFailed {
-        participant: RemoteParticipant,
-        error: String,
-        #[cfg(not(target_os = "windows"))]
-        track_sid: TrackSid,
-    },
-    TrackPublished {
-        publication: RemoteTrackPublication,
-        participant: RemoteParticipant,
-    },
-    TrackUnpublished {
-        publication: RemoteTrackPublication,
-        participant: RemoteParticipant,
-    },
-    TrackMuted {
-        participant: Participant,
-        publication: TrackPublication,
-    },
-    TrackUnmuted {
-        participant: Participant,
-        publication: TrackPublication,
-    },
-    RoomMetadataChanged {
-        old_metadata: String,
-        metadata: String,
-    },
-    ParticipantMetadataChanged {
-        participant: Participant,
-        old_metadata: String,
-        metadata: String,
-    },
-    ParticipantNameChanged {
-        participant: Participant,
-        old_name: String,
-        name: String,
-    },
-    ActiveSpeakersChanged {
-        speakers: Vec<Participant>,
-    },
-    #[cfg(not(target_os = "windows"))]
-    ConnectionStateChanged(ConnectionState),
-    Connected {
-        participants_with_tracks: Vec<(RemoteParticipant, Vec<RemoteTrackPublication>)>,
-    },
-    #[cfg(not(target_os = "windows"))]
-    Disconnected {
-        reason: DisconnectReason,
-    },
-    Reconnecting,
-    Reconnected,
-}
-
-#[cfg(not(target_os = "windows"))]
 #[async_trait]
 impl live_kit_server::api::Client for TestApiClient {
     fn url(&self) -> &str {
@@ -660,9 +474,7 @@ impl live_kit_server::api::Client for TestApiClient {
 
     async fn remove_participant(&self, room: String, identity: String) -> Result<()> {
         let server = TestServer::get(&self.url)?;
-        server
-            .remove_participant(room, ParticipantIdentity(identity))
-            .await?;
+        server.remove_participant(room, identity).await?;
         Ok(())
     }
 
@@ -701,125 +513,370 @@ impl live_kit_server::api::Client for TestApiClient {
 }
 
 struct RoomState {
-    url: String,
-    token: String,
-    #[cfg(not(target_os = "windows"))]
-    local_identity: ParticipantIdentity,
-    #[cfg(not(target_os = "windows"))]
-    connection_state: ConnectionState,
-    #[cfg(not(target_os = "windows"))]
-    paused_audio_tracks: HashSet<TrackSid>,
-    updates_tx: mpsc::Sender<RoomEvent>,
+    connection: (
+        watch::Sender<ConnectionState>,
+        watch::Receiver<ConnectionState>,
+    ),
+    display_sources: Vec<MacOSDisplay>,
+    paused_audio_tracks: HashSet<Sid>,
+    updates_tx: async_broadcast::Sender<RoomUpdate>,
+    updates_rx: async_broadcast::Receiver<RoomUpdate>,
 }
 
-#[derive(Clone, Debug)]
-pub struct Room(Arc<Mutex<RoomState>>);
+pub struct Room(Mutex<RoomState>);
 
-#[derive(Clone, Debug)]
-pub(crate) struct WeakRoom(Weak<Mutex<RoomState>>);
+impl Room {
+    pub fn new() -> Arc<Self> {
+        let (updates_tx, updates_rx) = async_broadcast::broadcast(128);
+        Arc::new(Self(Mutex::new(RoomState {
+            connection: watch::channel_with(ConnectionState::Disconnected),
+            display_sources: Default::default(),
+            paused_audio_tracks: Default::default(),
+            updates_tx,
+            updates_rx,
+        })))
+    }
 
-#[cfg(not(target_os = "windows"))]
-impl std::fmt::Debug for RoomState {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("Room")
-            .field("url", &self.url)
-            .field("token", &self.token)
-            .field("local_identity", &self.local_identity)
-            .field("connection_state", &self.connection_state)
-            .field("paused_audio_tracks", &self.paused_audio_tracks)
-            .finish()
+    pub fn status(&self) -> watch::Receiver<ConnectionState> {
+        self.0.lock().connection.1.clone()
     }
-}
 
-#[cfg(target_os = "windows")]
-impl std::fmt::Debug for RoomState {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("Room")
-            .field("url", &self.url)
-            .field("token", &self.token)
-            .finish()
+    pub fn connect(self: &Arc<Self>, url: &str, token: &str) -> impl Future<Output = Result<()>> {
+        let this = self.clone();
+        let url = url.to_string();
+        let token = token.to_string();
+        async move {
+            let server = TestServer::get(&url)?;
+            server
+                .join_room(token.clone(), this.clone())
+                .await
+                .context("room join")?;
+            *this.0.lock().connection.0.borrow_mut() = ConnectionState::Connected { url, token };
+            Ok(())
+        }
     }
-}
 
-#[cfg(not(target_os = "windows"))]
-impl Room {
-    fn downgrade(&self) -> WeakRoom {
-        WeakRoom(Arc::downgrade(&self.0))
+    pub fn display_sources(self: &Arc<Self>) -> impl Future<Output = Result<Vec<MacOSDisplay>>> {
+        let this = self.clone();
+        async move {
+            // todo(linux): Remove this once the cross-platform LiveKit implementation is merged
+            #[cfg(any(test, feature = "test-support"))]
+            {
+                let server = this.test_server();
+                server.executor.simulate_random_delay().await;
+            }
+
+            Ok(this.0.lock().display_sources.clone())
+        }
     }
 
-    pub fn connection_state(&self) -> ConnectionState {
-        self.0.lock().connection_state
+    pub fn publish_video_track(
+        self: &Arc<Self>,
+        track: LocalVideoTrack,
+    ) -> impl Future<Output = Result<LocalTrackPublication>> {
+        let this = self.clone();
+        let track = track.clone();
+        async move {
+            let sid = this
+                .test_server()
+                .publish_video_track(this.token(), track)
+                .await?;
+            Ok(LocalTrackPublication {
+                room: Arc::downgrade(&this),
+                sid,
+            })
+        }
     }
 
-    pub fn local_participant(&self) -> LocalParticipant {
-        let identity = self.0.lock().local_identity.clone();
-        LocalParticipant {
-            identity,
-            room: self.clone(),
+    pub fn publish_audio_track(
+        self: &Arc<Self>,
+        track: LocalAudioTrack,
+    ) -> impl Future<Output = Result<LocalTrackPublication>> {
+        let this = self.clone();
+        let track = track.clone();
+        async move {
+            let sid = this
+                .test_server()
+                .publish_audio_track(this.token(), &track)
+                .await?;
+            Ok(LocalTrackPublication {
+                room: Arc::downgrade(&this),
+                sid,
+            })
         }
     }
 
-    pub async fn connect(
-        url: &str,
-        token: &str,
-        _options: RoomOptions,
-    ) -> Result<(Self, mpsc::Receiver<RoomEvent>)> {
-        let server = TestServer::get(&url)?;
-        let (updates_tx, updates_rx) = mpsc::channel(1024);
-        let this = Self(Arc::new(Mutex::new(RoomState {
-            local_identity: ParticipantIdentity(String::new()),
-            url: url.to_string(),
-            token: token.to_string(),
-            connection_state: ConnectionState::Disconnected,
-            paused_audio_tracks: Default::default(),
-            updates_tx,
-        })));
+    pub fn unpublish_track(&self, _publication: LocalTrackPublication) {}
 
-        let identity = server
-            .join_room(token.to_string(), this.clone())
-            .await
-            .context("room join")?;
-        {
-            let mut state = this.0.lock();
-            state.local_identity = identity;
-            state.connection_state = ConnectionState::Connected;
+    pub fn remote_audio_tracks(&self, publisher_id: &str) -> Vec<Arc<RemoteAudioTrack>> {
+        if !self.is_connected() {
+            return Vec::new();
+        }
+
+        self.test_server()
+            .audio_tracks(self.token())
+            .unwrap()
+            .into_iter()
+            .filter(|track| track.publisher_id() == publisher_id)
+            .collect()
+    }
+
+    pub fn remote_audio_track_publications(
+        &self,
+        publisher_id: &str,
+    ) -> Vec<Arc<RemoteTrackPublication>> {
+        if !self.is_connected() {
+            return Vec::new();
         }
 
-        Ok((this, updates_rx))
+        self.test_server()
+            .audio_tracks(self.token())
+            .unwrap()
+            .into_iter()
+            .filter(|track| track.publisher_id() == publisher_id)
+            .map(|_track| Arc::new(RemoteTrackPublication {}))
+            .collect()
     }
 
-    pub fn remote_participants(&self) -> HashMap<ParticipantIdentity, RemoteParticipant> {
+    pub fn remote_video_tracks(&self, publisher_id: &str) -> Vec<Arc<RemoteVideoTrack>> {
+        if !self.is_connected() {
+            return Vec::new();
+        }
+
         self.test_server()
-            .remote_participants(self.0.lock().token.clone())
+            .video_tracks(self.token())
             .unwrap()
+            .into_iter()
+            .filter(|track| track.publisher_id() == publisher_id)
+            .collect()
+    }
+
+    pub fn updates(&self) -> impl Stream<Item = RoomUpdate> {
+        self.0.lock().updates_rx.clone()
+    }
+
+    pub fn set_display_sources(&self, sources: Vec<MacOSDisplay>) {
+        self.0.lock().display_sources = sources;
     }
 
     fn test_server(&self) -> Arc<TestServer> {
-        TestServer::get(&self.0.lock().url).unwrap()
+        match self.0.lock().connection.1.borrow().clone() {
+            ConnectionState::Disconnected => panic!("must be connected to call this method"),
+            ConnectionState::Connected { url, .. } => TestServer::get(&url).unwrap(),
+        }
     }
 
     fn token(&self) -> String {
-        self.0.lock().token.clone()
+        match self.0.lock().connection.1.borrow().clone() {
+            ConnectionState::Disconnected => panic!("must be connected to call this method"),
+            ConnectionState::Connected { token, .. } => token,
+        }
+    }
+
+    fn is_connected(&self) -> bool {
+        match *self.0.lock().connection.1.borrow() {
+            ConnectionState::Disconnected => false,
+            ConnectionState::Connected { .. } => true,
+        }
     }
 }
 
-#[cfg(not(target_os = "windows"))]
-impl Drop for RoomState {
+impl Drop for Room {
     fn drop(&mut self) {
-        if self.connection_state == ConnectionState::Connected {
-            if let Ok(server) = TestServer::get(&self.url) {
+        if let ConnectionState::Connected { token, .. } = mem::replace(
+            &mut *self.0.lock().connection.0.borrow_mut(),
+            ConnectionState::Disconnected,
+        ) {
+            if let Ok(server) = TestServer::get(&token) {
                 let executor = server.executor.clone();
-                let token = self.token.clone();
                 executor
-                    .spawn(async move { server.leave_room(token).await.ok() })
+                    .spawn(async move { server.leave_room(token).await.unwrap() })
                     .detach();
             }
         }
     }
 }
 
-impl WeakRoom {
-    fn upgrade(&self) -> Option<Room> {
-        self.0.upgrade().map(Room)
+#[derive(Clone)]
+pub struct LocalTrackPublication {
+    sid: String,
+    room: Weak<Room>,
+}
+
+impl LocalTrackPublication {
+    pub fn set_mute(&self, mute: bool) -> impl Future<Output = Result<()>> {
+        let sid = self.sid.clone();
+        let room = self.room.clone();
+        async move {
+            if let Some(room) = room.upgrade() {
+                room.test_server()
+                    .set_track_muted(&room.token(), &sid, mute)
+            } else {
+                Err(anyhow!("no such room"))
+            }
+        }
+    }
+
+    pub fn is_muted(&self) -> bool {
+        if let Some(room) = self.room.upgrade() {
+            room.test_server()
+                .is_track_muted(&room.token(), &self.sid)
+                .unwrap_or(false)
+        } else {
+            false
+        }
+    }
+
+    pub fn sid(&self) -> String {
+        self.sid.clone()
+    }
+}
+
+pub struct RemoteTrackPublication;
+
+impl RemoteTrackPublication {
+    pub fn set_enabled(&self, _enabled: bool) -> impl Future<Output = Result<()>> {
+        async { Ok(()) }
+    }
+
+    pub fn is_muted(&self) -> bool {
+        false
+    }
+
+    pub fn sid(&self) -> String {
+        "".to_string()
+    }
+}
+
+#[derive(Clone)]
+pub struct LocalVideoTrack {
+    frames_rx: async_broadcast::Receiver<Frame>,
+}
+
+impl LocalVideoTrack {
+    pub fn screen_share_for_display(display: &MacOSDisplay) -> Self {
+        Self {
+            frames_rx: display.frames.1.clone(),
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct LocalAudioTrack;
+
+impl LocalAudioTrack {
+    pub fn create() -> Self {
+        Self
+    }
+}
+
+#[derive(Debug)]
+pub struct RemoteVideoTrack {
+    server_track: Arc<TestServerVideoTrack>,
+}
+
+impl RemoteVideoTrack {
+    pub fn sid(&self) -> &str {
+        &self.server_track.sid
+    }
+
+    pub fn publisher_id(&self) -> &str {
+        &self.server_track.publisher_id
+    }
+
+    pub fn frames(&self) -> async_broadcast::Receiver<Frame> {
+        self.server_track.frames_rx.clone()
+    }
+}
+
+#[derive(Debug)]
+pub struct RemoteAudioTrack {
+    server_track: Arc<TestServerAudioTrack>,
+    room: Weak<Room>,
+}
+
+impl RemoteAudioTrack {
+    pub fn sid(&self) -> &str {
+        &self.server_track.sid
+    }
+
+    pub fn publisher_id(&self) -> &str {
+        &self.server_track.publisher_id
+    }
+
+    pub fn start(&self) {
+        if let Some(room) = self.room.upgrade() {
+            room.0
+                .lock()
+                .paused_audio_tracks
+                .remove(&self.server_track.sid);
+        }
+    }
+
+    pub fn stop(&self) {
+        if let Some(room) = self.room.upgrade() {
+            room.0
+                .lock()
+                .paused_audio_tracks
+                .insert(self.server_track.sid.clone());
+        }
+    }
+
+    pub fn is_playing(&self) -> bool {
+        !self
+            .room
+            .upgrade()
+            .unwrap()
+            .0
+            .lock()
+            .paused_audio_tracks
+            .contains(&self.server_track.sid)
+    }
+}
+
+#[derive(Clone)]
+pub struct MacOSDisplay {
+    frames: (
+        async_broadcast::Sender<Frame>,
+        async_broadcast::Receiver<Frame>,
+    ),
+}
+
+impl Default for MacOSDisplay {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl MacOSDisplay {
+    pub fn new() -> Self {
+        Self {
+            frames: async_broadcast::broadcast(128),
+        }
+    }
+
+    pub fn send_frame(&self, frame: Frame) {
+        self.frames.0.try_broadcast(frame).unwrap();
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Frame {
+    pub label: String,
+    pub width: usize,
+    pub height: usize,
+}
+
+impl Frame {
+    pub fn width(&self) -> usize {
+        self.width
+    }
+
+    pub fn height(&self) -> usize {
+        self.height
+    }
+
+    pub fn image(&self) -> SurfaceSource {
+        unimplemented!("you can't call this in test mode")
     }
 }

crates/live_kit_client/src/test/participant.rs 🔗

@@ -1,111 +0,0 @@
-use super::*;
-
-#[derive(Clone, Debug)]
-pub enum Participant {
-    Local(LocalParticipant),
-    Remote(RemoteParticipant),
-}
-
-#[derive(Clone, Debug)]
-pub struct LocalParticipant {
-    #[cfg(not(target_os = "windows"))]
-    pub(super) identity: ParticipantIdentity,
-    pub(super) room: Room,
-}
-
-#[derive(Clone, Debug)]
-pub struct RemoteParticipant {
-    #[cfg(not(target_os = "windows"))]
-    pub(super) identity: ParticipantIdentity,
-    pub(super) room: WeakRoom,
-}
-
-#[cfg(not(target_os = "windows"))]
-impl Participant {
-    pub fn identity(&self) -> ParticipantIdentity {
-        match self {
-            Participant::Local(participant) => participant.identity.clone(),
-            Participant::Remote(participant) => participant.identity.clone(),
-        }
-    }
-}
-
-#[cfg(not(target_os = "windows"))]
-impl LocalParticipant {
-    pub async fn unpublish_track(&self, track: &TrackSid) -> Result<()> {
-        self.room
-            .test_server()
-            .unpublish_track(self.room.token(), track)
-            .await
-    }
-
-    pub async fn publish_track(
-        &self,
-        track: LocalTrack,
-        _options: TrackPublishOptions,
-    ) -> Result<LocalTrackPublication> {
-        let this = self.clone();
-        let track = track.clone();
-        let server = this.room.test_server();
-        let sid = match track {
-            LocalTrack::Video(track) => {
-                server.publish_video_track(this.room.token(), track).await?
-            }
-            LocalTrack::Audio(track) => {
-                server
-                    .publish_audio_track(this.room.token(), &track)
-                    .await?
-            }
-        };
-        Ok(LocalTrackPublication {
-            room: self.room.downgrade(),
-            sid,
-        })
-    }
-}
-
-#[cfg(not(target_os = "windows"))]
-impl RemoteParticipant {
-    pub fn track_publications(&self) -> HashMap<TrackSid, RemoteTrackPublication> {
-        if let Some(room) = self.room.upgrade() {
-            let server = room.test_server();
-            let audio = server
-                .audio_tracks(room.token())
-                .unwrap()
-                .into_iter()
-                .filter(|track| track.publisher_id() == self.identity)
-                .map(|track| {
-                    (
-                        track.sid(),
-                        RemoteTrackPublication {
-                            sid: track.sid(),
-                            room: self.room.clone(),
-                            track: RemoteTrack::Audio(track),
-                        },
-                    )
-                });
-            let video = server
-                .video_tracks(room.token())
-                .unwrap()
-                .into_iter()
-                .filter(|track| track.publisher_id() == self.identity)
-                .map(|track| {
-                    (
-                        track.sid(),
-                        RemoteTrackPublication {
-                            sid: track.sid(),
-                            room: self.room.clone(),
-                            track: RemoteTrack::Video(track),
-                        },
-                    )
-                });
-            audio.chain(video).collect()
-        } else {
-            HashMap::default()
-        }
-    }
-
-    pub fn identity(&self) -> ParticipantIdentity {
-        self.identity.clone()
-    }
-}

crates/live_kit_client/src/test/publication.rs 🔗

@@ -1,116 +0,0 @@
-use super::*;
-
-#[derive(Clone, Debug)]
-pub enum TrackPublication {
-    Local(LocalTrackPublication),
-    Remote(RemoteTrackPublication),
-}
-
-#[derive(Clone, Debug)]
-pub struct LocalTrackPublication {
-    #[cfg(not(target_os = "windows"))]
-    pub(crate) sid: TrackSid,
-    pub(crate) room: WeakRoom,
-}
-
-#[derive(Clone, Debug)]
-pub struct RemoteTrackPublication {
-    #[cfg(not(target_os = "windows"))]
-    pub(crate) sid: TrackSid,
-    pub(crate) room: WeakRoom,
-    pub(crate) track: RemoteTrack,
-}
-
-#[cfg(not(target_os = "windows"))]
-impl TrackPublication {
-    pub fn sid(&self) -> TrackSid {
-        match self {
-            TrackPublication::Local(track) => track.sid(),
-            TrackPublication::Remote(track) => track.sid(),
-        }
-    }
-
-    pub fn is_muted(&self) -> bool {
-        match self {
-            TrackPublication::Local(track) => track.is_muted(),
-            TrackPublication::Remote(track) => track.is_muted(),
-        }
-    }
-}
-
-#[cfg(not(target_os = "windows"))]
-impl LocalTrackPublication {
-    pub fn sid(&self) -> TrackSid {
-        self.sid.clone()
-    }
-
-    pub fn mute(&self) {
-        self.set_mute(true)
-    }
-
-    pub fn unmute(&self) {
-        self.set_mute(false)
-    }
-
-    fn set_mute(&self, mute: bool) {
-        if let Some(room) = self.room.upgrade() {
-            room.test_server()
-                .set_track_muted(&room.token(), &self.sid, mute)
-                .ok();
-        }
-    }
-
-    pub fn is_muted(&self) -> bool {
-        if let Some(room) = self.room.upgrade() {
-            room.test_server()
-                .is_track_muted(&room.token(), &self.sid)
-                .unwrap_or(false)
-        } else {
-            false
-        }
-    }
-}
-
-#[cfg(not(target_os = "windows"))]
-impl RemoteTrackPublication {
-    pub fn sid(&self) -> TrackSid {
-        self.sid.clone()
-    }
-
-    pub fn track(&self) -> Option<RemoteTrack> {
-        Some(self.track.clone())
-    }
-
-    pub fn kind(&self) -> TrackKind {
-        self.track.kind()
-    }
-
-    pub fn is_muted(&self) -> bool {
-        if let Some(room) = self.room.upgrade() {
-            room.test_server()
-                .is_track_muted(&room.token(), &self.sid)
-                .unwrap_or(false)
-        } else {
-            false
-        }
-    }
-
-    pub fn is_enabled(&self) -> bool {
-        if let Some(room) = self.room.upgrade() {
-            !room.0.lock().paused_audio_tracks.contains(&self.sid)
-        } else {
-            false
-        }
-    }
-
-    pub fn set_enabled(&self, enabled: bool) {
-        if let Some(room) = self.room.upgrade() {
-            let paused_audio_tracks = &mut room.0.lock().paused_audio_tracks;
-            if enabled {
-                paused_audio_tracks.remove(&self.sid);
-            } else {
-                paused_audio_tracks.insert(self.sid.clone());
-            }
-        }
-    }
-}

crates/live_kit_client/src/test/track.rs 🔗

@@ -1,201 +0,0 @@
-use super::*;
-#[cfg(not(windows))]
-use webrtc::{audio_source::RtcAudioSource, video_source::RtcVideoSource};
-
-#[cfg(not(windows))]
-pub use livekit::track::{TrackKind, TrackSource};
-
-#[derive(Clone, Debug)]
-pub enum LocalTrack {
-    Audio(LocalAudioTrack),
-    Video(LocalVideoTrack),
-}
-
-#[derive(Clone, Debug)]
-pub enum RemoteTrack {
-    Audio(RemoteAudioTrack),
-    Video(RemoteVideoTrack),
-}
-
-#[derive(Clone, Debug)]
-pub struct LocalVideoTrack {}
-
-#[derive(Clone, Debug)]
-pub struct LocalAudioTrack {}
-
-#[derive(Clone, Debug)]
-pub struct RemoteVideoTrack {
-    #[cfg(not(target_os = "windows"))]
-    pub(super) server_track: Arc<TestServerVideoTrack>,
-    pub(super) _room: WeakRoom,
-}
-
-#[derive(Clone, Debug)]
-pub struct RemoteAudioTrack {
-    #[cfg(not(target_os = "windows"))]
-    pub(super) server_track: Arc<TestServerAudioTrack>,
-    pub(super) room: WeakRoom,
-}
-
-pub enum RtcTrack {
-    Audio(RtcAudioTrack),
-    Video(RtcVideoTrack),
-}
-
-pub struct RtcAudioTrack {
-    #[cfg(not(target_os = "windows"))]
-    pub(super) server_track: Arc<TestServerAudioTrack>,
-    pub(super) room: WeakRoom,
-}
-
-pub struct RtcVideoTrack {
-    #[cfg(not(target_os = "windows"))]
-    pub(super) _server_track: Arc<TestServerVideoTrack>,
-}
-
-#[cfg(not(target_os = "windows"))]
-impl RemoteTrack {
-    pub fn sid(&self) -> TrackSid {
-        match self {
-            RemoteTrack::Audio(track) => track.sid(),
-            RemoteTrack::Video(track) => track.sid(),
-        }
-    }
-
-    pub fn kind(&self) -> TrackKind {
-        match self {
-            RemoteTrack::Audio(_) => TrackKind::Audio,
-            RemoteTrack::Video(_) => TrackKind::Video,
-        }
-    }
-
-    pub fn publisher_id(&self) -> ParticipantIdentity {
-        match self {
-            RemoteTrack::Audio(track) => track.publisher_id(),
-            RemoteTrack::Video(track) => track.publisher_id(),
-        }
-    }
-
-    pub fn rtc_track(&self) -> RtcTrack {
-        match self {
-            RemoteTrack::Audio(track) => RtcTrack::Audio(track.rtc_track()),
-            RemoteTrack::Video(track) => RtcTrack::Video(track.rtc_track()),
-        }
-    }
-}
-
-#[cfg(not(windows))]
-impl LocalVideoTrack {
-    pub fn create_video_track(_name: &str, _source: RtcVideoSource) -> Self {
-        Self {}
-    }
-}
-
-#[cfg(not(windows))]
-impl LocalAudioTrack {
-    pub fn create_audio_track(_name: &str, _source: RtcAudioSource) -> Self {
-        Self {}
-    }
-}
-
-#[cfg(not(target_os = "windows"))]
-impl RemoteAudioTrack {
-    pub fn sid(&self) -> TrackSid {
-        self.server_track.sid.clone()
-    }
-
-    pub fn publisher_id(&self) -> ParticipantIdentity {
-        self.server_track.publisher_id.clone()
-    }
-
-    pub fn start(&self) {
-        if let Some(room) = self.room.upgrade() {
-            room.0
-                .lock()
-                .paused_audio_tracks
-                .remove(&self.server_track.sid);
-        }
-    }
-
-    pub fn stop(&self) {
-        if let Some(room) = self.room.upgrade() {
-            room.0
-                .lock()
-                .paused_audio_tracks
-                .insert(self.server_track.sid.clone());
-        }
-    }
-
-    pub fn rtc_track(&self) -> RtcAudioTrack {
-        RtcAudioTrack {
-            server_track: self.server_track.clone(),
-            room: self.room.clone(),
-        }
-    }
-}
-
-#[cfg(not(target_os = "windows"))]
-impl RemoteVideoTrack {
-    pub fn sid(&self) -> TrackSid {
-        self.server_track.sid.clone()
-    }
-
-    pub fn publisher_id(&self) -> ParticipantIdentity {
-        self.server_track.publisher_id.clone()
-    }
-
-    pub fn rtc_track(&self) -> RtcVideoTrack {
-        RtcVideoTrack {
-            _server_track: self.server_track.clone(),
-        }
-    }
-}
-
-#[cfg(not(target_os = "windows"))]
-impl RtcTrack {
-    pub fn enabled(&self) -> bool {
-        match self {
-            RtcTrack::Audio(track) => track.enabled(),
-            RtcTrack::Video(track) => track.enabled(),
-        }
-    }
-
-    pub fn set_enabled(&self, enabled: bool) {
-        match self {
-            RtcTrack::Audio(track) => track.set_enabled(enabled),
-            RtcTrack::Video(_) => {}
-        }
-    }
-}
-
-#[cfg(not(target_os = "windows"))]
-impl RtcAudioTrack {
-    pub fn set_enabled(&self, enabled: bool) {
-        if let Some(room) = self.room.upgrade() {
-            let paused_audio_tracks = &mut room.0.lock().paused_audio_tracks;
-            if enabled {
-                paused_audio_tracks.remove(&self.server_track.sid);
-            } else {
-                paused_audio_tracks.insert(self.server_track.sid.clone());
-            }
-        }
-    }
-
-    pub fn enabled(&self) -> bool {
-        if let Some(room) = self.room.upgrade() {
-            !room
-                .0
-                .lock()
-                .paused_audio_tracks
-                .contains(&self.server_track.sid)
-        } else {
-            false
-        }
-    }
-}
-
-impl RtcVideoTrack {
-    pub fn enabled(&self) -> bool {
-        true
-    }
-}

crates/live_kit_client/src/test/webrtc.rs 🔗

@@ -1,136 +0,0 @@
-use super::track::{RtcAudioTrack, RtcVideoTrack};
-use futures::Stream;
-use livekit::webrtc as real;
-use std::{
-    pin::Pin,
-    task::{Context, Poll},
-};
-
-pub mod video_stream {
-    use super::*;
-
-    pub mod native {
-        use super::*;
-        use real::video_frame::BoxVideoFrame;
-
-        pub struct NativeVideoStream {
-            pub track: RtcVideoTrack,
-        }
-
-        impl NativeVideoStream {
-            pub fn new(track: RtcVideoTrack) -> Self {
-                Self { track }
-            }
-        }
-
-        impl Stream for NativeVideoStream {
-            type Item = BoxVideoFrame;
-
-            fn poll_next(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Option<Self::Item>> {
-                Poll::Pending
-            }
-        }
-    }
-}
-
-pub mod audio_stream {
-    use super::*;
-
-    pub mod native {
-        use super::*;
-        use real::audio_frame::AudioFrame;
-
-        pub struct NativeAudioStream {
-            pub track: RtcAudioTrack,
-        }
-
-        impl NativeAudioStream {
-            pub fn new(track: RtcAudioTrack, _sample_rate: i32, _num_channels: i32) -> Self {
-                Self { track }
-            }
-        }
-
-        impl Stream for NativeAudioStream {
-            type Item = AudioFrame<'static>;
-
-            fn poll_next(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Option<Self::Item>> {
-                Poll::Pending
-            }
-        }
-    }
-}
-
-pub mod audio_source {
-    use super::*;
-
-    pub use real::audio_source::AudioSourceOptions;
-
-    pub mod native {
-        use std::sync::Arc;
-
-        use super::*;
-        use real::{audio_frame::AudioFrame, RtcError};
-
-        #[derive(Clone)]
-        pub struct NativeAudioSource {
-            pub options: Arc<AudioSourceOptions>,
-            pub sample_rate: u32,
-            pub num_channels: u32,
-        }
-
-        impl NativeAudioSource {
-            pub fn new(
-                options: AudioSourceOptions,
-                sample_rate: u32,
-                num_channels: u32,
-                _queue_size_ms: u32,
-            ) -> Self {
-                Self {
-                    options: Arc::new(options),
-                    sample_rate,
-                    num_channels,
-                }
-            }
-
-            pub async fn capture_frame(&self, _frame: &AudioFrame<'_>) -> Result<(), RtcError> {
-                Ok(())
-            }
-        }
-    }
-
-    pub enum RtcAudioSource {
-        Native(native::NativeAudioSource),
-    }
-}
-
-pub use livekit::webrtc::audio_frame;
-pub use livekit::webrtc::video_frame;
-
-pub mod video_source {
-    use super::*;
-    pub use real::video_source::VideoResolution;
-
-    pub struct RTCVideoSource;
-
-    pub mod native {
-        use super::*;
-        use real::video_frame::{VideoBuffer, VideoFrame};
-
-        #[derive(Clone)]
-        pub struct NativeVideoSource {
-            pub resolution: VideoResolution,
-        }
-
-        impl NativeVideoSource {
-            pub fn new(resolution: super::VideoResolution) -> Self {
-                Self { resolution }
-            }
-
-            pub fn capture_frame<T: AsRef<dyn VideoBuffer>>(&self, _frame: &VideoFrame<T>) {}
-        }
-    }
-
-    pub enum RtcVideoSource {
-        Native(native::NativeVideoSource),
-    }
-}

crates/media/Cargo.toml 🔗

@@ -17,7 +17,6 @@ anyhow.workspace = true
 
 [target.'cfg(target_os = "macos")'.dependencies]
 core-foundation.workspace = true
-ctor.workspace = true
 foreign-types = "0.5"
 metal = "0.29"
 objc = "0.2"

crates/media/src/media.rs 🔗

@@ -253,14 +253,11 @@ pub mod core_media {
             }
         }
 
-        pub fn image_buffer(&self) -> Option<CVImageBuffer> {
+        pub fn image_buffer(&self) -> CVImageBuffer {
             unsafe {
-                let ptr = CMSampleBufferGetImageBuffer(self.as_concrete_TypeRef());
-                if ptr.is_null() {
-                    None
-                } else {
-                    Some(CVImageBuffer::wrap_under_get_rule(ptr))
-                }
+                CVImageBuffer::wrap_under_get_rule(CMSampleBufferGetImageBuffer(
+                    self.as_concrete_TypeRef(),
+                ))
             }
         }
 

crates/title_bar/src/collab.rs 🔗

@@ -296,9 +296,9 @@ impl TitleBar {
         let is_muted = room.is_muted();
         let is_deafened = room.is_deafened().unwrap_or(false);
         let is_screen_sharing = room.is_screen_sharing();
-        let can_use_microphone = room.can_use_microphone(cx);
+        let can_use_microphone = room.can_use_microphone();
         let can_share_projects = room.can_share_projects();
-        let screen_sharing_supported = match self.platform_style {
+        let platform_supported = match self.platform_style {
             PlatformStyle::Mac => true,
             PlatformStyle::Linux | PlatformStyle::Windows => false,
         };
@@ -365,7 +365,9 @@ impl TitleBar {
                 )
                 .tooltip(move |cx| {
                     Tooltip::text(
-                        if is_muted {
+                        if !platform_supported {
+                            "Cannot share microphone"
+                        } else if is_muted {
                             "Unmute microphone"
                         } else {
                             "Mute microphone"
@@ -375,45 +377,56 @@ impl TitleBar {
                 })
                 .style(ButtonStyle::Subtle)
                 .icon_size(IconSize::Small)
-                .selected(is_muted)
+                .selected(platform_supported && is_muted)
+                .disabled(!platform_supported)
                 .selected_style(ButtonStyle::Tinted(TintColor::Negative))
                 .on_click(move |_, cx| {
                     toggle_mute(&Default::default(), cx);
                 })
                 .into_any_element(),
             );
+        }
 
-            children.push(
-                IconButton::new(
-                    "mute-sound",
-                    if is_deafened {
-                        ui::IconName::AudioOff
-                    } else {
-                        ui::IconName::AudioOn
-                    },
-                )
-                .style(ButtonStyle::Subtle)
-                .selected_style(ButtonStyle::Tinted(TintColor::Negative))
-                .icon_size(IconSize::Small)
-                .selected(is_deafened)
-                .tooltip(move |cx| {
+        children.push(
+            IconButton::new(
+                "mute-sound",
+                if is_deafened {
+                    ui::IconName::AudioOff
+                } else {
+                    ui::IconName::AudioOn
+                },
+            )
+            .style(ButtonStyle::Subtle)
+            .selected_style(ButtonStyle::Tinted(TintColor::Negative))
+            .icon_size(IconSize::Small)
+            .selected(is_deafened)
+            .disabled(!platform_supported)
+            .tooltip(move |cx| {
+                if !platform_supported {
+                    Tooltip::text("Cannot share microphone", cx)
+                } else if can_use_microphone {
                     Tooltip::with_meta("Deafen Audio", None, "Mic will be muted", cx)
-                })
-                .on_click(move |_, cx| toggle_deafen(&Default::default(), cx))
-                .into_any_element(),
-            );
-        }
+                } else {
+                    Tooltip::text("Deafen Audio", cx)
+                }
+            })
+            .on_click(move |_, cx| toggle_deafen(&Default::default(), cx))
+            .into_any_element(),
+        );
 
-        if screen_sharing_supported {
+        if can_share_projects {
             children.push(
                 IconButton::new("screen-share", ui::IconName::Screen)
                     .style(ButtonStyle::Subtle)
                     .icon_size(IconSize::Small)
                     .selected(is_screen_sharing)
+                    .disabled(!platform_supported)
                     .selected_style(ButtonStyle::Tinted(TintColor::Accent))
                     .tooltip(move |cx| {
                         Tooltip::text(
-                            if is_screen_sharing {
+                            if !platform_supported {
+                                "Cannot share screen"
+                            } else if is_screen_sharing {
                                 "Stop Sharing Screen"
                             } else {
                                 "Share Screen"

crates/workspace/src/shared_screen.rs 🔗

@@ -2,13 +2,16 @@ use crate::{
     item::{Item, ItemEvent},
     ItemNavHistory, WorkspaceId,
 };
-use call::{RemoteVideoTrack, RemoteVideoTrackView};
+use anyhow::Result;
+use call::participant::{Frame, RemoteVideoTrack};
 use client::{proto::PeerId, User};
+use futures::StreamExt;
 use gpui::{
-    div, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement, ParentElement,
-    Render, SharedString, Styled, View, ViewContext, VisualContext, WindowContext,
+    div, surface, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
+    ParentElement, Render, SharedString, Styled, Task, View, ViewContext, VisualContext,
+    WindowContext,
 };
-use std::sync::Arc;
+use std::sync::{Arc, Weak};
 use ui::{prelude::*, Icon, IconName};
 
 pub enum Event {
@@ -16,30 +19,40 @@ pub enum Event {
 }
 
 pub struct SharedScreen {
+    track: Weak<RemoteVideoTrack>,
+    frame: Option<Frame>,
     pub peer_id: PeerId,
     user: Arc<User>,
     nav_history: Option<ItemNavHistory>,
-    view: View<RemoteVideoTrackView>,
+    _maintain_frame: Task<Result<()>>,
     focus: FocusHandle,
 }
 
 impl SharedScreen {
     pub fn new(
-        track: RemoteVideoTrack,
+        track: &Arc<RemoteVideoTrack>,
         peer_id: PeerId,
         user: Arc<User>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        let view = cx.new_view(|cx| RemoteVideoTrackView::new(track.clone(), cx));
-        cx.subscribe(&view, |_, _, ev, cx| match ev {
-            call::RemoteVideoTrackViewEvent::Close => cx.emit(Event::Close),
-        })
-        .detach();
+        cx.focus_handle();
+        let mut frames = track.frames();
         Self {
-            view,
+            track: Arc::downgrade(track),
+            frame: None,
             peer_id,
             user,
             nav_history: Default::default(),
+            _maintain_frame: cx.spawn(|this, mut cx| async move {
+                while let Some(frame) = frames.next().await {
+                    this.update(&mut cx, |this, cx| {
+                        this.frame = Some(frame);
+                        cx.notify();
+                    })?;
+                }
+                this.update(&mut cx, |_, cx| cx.emit(Event::Close))?;
+                Ok(())
+            }),
             focus: cx.focus_handle(),
         }
     }
@@ -59,7 +72,11 @@ impl Render for SharedScreen {
             .track_focus(&self.focus)
             .key_context("SharedScreen")
             .size_full()
-            .child(self.view.clone())
+            .children(
+                self.frame
+                    .as_ref()
+                    .map(|frame| surface(frame.image()).size_full()),
+            )
     }
 }
 
@@ -97,13 +114,8 @@ impl Item for SharedScreen {
         _workspace_id: Option<WorkspaceId>,
         cx: &mut ViewContext<Self>,
     ) -> Option<View<Self>> {
-        Some(cx.new_view(|cx| Self {
-            view: self.view.update(cx, |view, cx| view.clone(cx)),
-            peer_id: self.peer_id,
-            user: self.user.clone(),
-            nav_history: Default::default(),
-            focus: cx.focus_handle(),
-        }))
+        let track = self.track.upgrade()?;
+        Some(cx.new_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx)))
     }
 
     fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {

crates/workspace/src/workspace.rs 🔗

@@ -3939,17 +3939,6 @@ impl Workspace {
         None
     }
 
-    #[cfg(target_os = "windows")]
-    fn shared_screen_for_peer(
-        &self,
-        _peer_id: PeerId,
-        _pane: &View<Pane>,
-        _cx: &mut WindowContext,
-    ) -> Option<View<SharedScreen>> {
-        None
-    }
-
-    #[cfg(not(target_os = "windows"))]
     fn shared_screen_for_peer(
         &self,
         peer_id: PeerId,
@@ -3968,7 +3957,7 @@ impl Workspace {
             }
         }
 
-        Some(cx.new_view(|cx| SharedScreen::new(track, peer_id, user.clone(), cx)))
+        Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
     }
 
     pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {