Detailed changes
@@ -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"]
@@ -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",
]
@@ -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"
@@ -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
@@ -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));
@@ -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>>,
}
@@ -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.
@@ -1,6 +1,3 @@
-// todo(windows): Actually run the tests
-#![cfg(not(target_os = "windows"))]
-
use std::sync::Arc;
use call::Room;
@@ -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()));
}
@@ -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();
@@ -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();
@@ -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 {
@@ -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,
@@ -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()
@@ -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()
@@ -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()
@@ -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 {
@@ -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);
@@ -20,5 +20,3 @@ pub(crate) use text_system::*;
pub(crate) use wayland::*;
#[cfg(feature = "x11")]
pub(crate) use x11::*;
-
-pub(crate) type PlatformScreenCaptureFrame = ();
@@ -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()
}
@@ -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;
}
@@ -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()
}
@@ -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);
- }
- }
-}
@@ -7,5 +7,3 @@ pub(crate) use dispatcher::*;
pub(crate) use display::*;
pub(crate) use platform::*;
pub(crate) use window::*;
-
-pub use platform::TestScreenCaptureSource;
@@ -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) {
@@ -21,5 +21,3 @@ pub(crate) use window::*;
pub(crate) use wrapper::*;
pub(crate) use windows::Win32::Foundation::HWND;
-
-pub(crate) type PlatformScreenCaptureFrame = ();
@@ -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)
@@ -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
@@ -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
@@ -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
+}
@@ -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")]),
+ ]
+)
@@ -0,0 +1,3 @@
+# LiveKitBridge
+
+A description of this package.
@@ -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
+}
@@ -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())
+ }
+}
@@ -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();
}
@@ -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 },
}
@@ -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()
+ }
+}
@@ -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()
- }
-}
@@ -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")
}
}
@@ -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()
- }
-}
@@ -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());
- }
- }
- }
-}
@@ -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
- }
-}
@@ -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),
- }
-}
@@ -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"
@@ -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(),
+ ))
}
}
@@ -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"
@@ -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)) {
@@ -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>) {