diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 866d0acc0eb40355a09a0811e216cb8c5c0eeba0..dab6b158c2d8a74d154c27404ff8e8b4b73fccd0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -39,6 +39,7 @@ jobs:
uses: actions/checkout@v2
with:
clean: false
+ submodules: 'recursive'
- name: Run tests
run: cargo test --workspace --no-fail-fast
@@ -57,6 +58,7 @@ jobs:
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
ZED_AMPLITUDE_API_KEY: ${{ secrets.ZED_AMPLITUDE_API_KEY }}
+ ZED_MIXPANEL_TOKEN: ${{ secrets.ZED_MIXPANEL_TOKEN }}
steps:
- name: Install Rust
run: |
@@ -75,6 +77,7 @@ jobs:
uses: actions/checkout@v2
with:
clean: false
+ submodules: 'recursive'
- name: Validate version
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml
index 9a3b2376df472eba470dc92fca7293d8a015206a..17cb8864e15f8c2994cb5c738d16458b89463f2c 100644
--- a/.github/workflows/release_actions.yml
+++ b/.github/workflows/release_actions.yml
@@ -30,4 +30,4 @@ jobs:
architecture: "x64"
cache: "pip"
- run: pip install -r script/amplitude_release/requirements.txt
- - run: python script/amplitude_release/main.py ${{ github.event.release.tag_name }} ${{ secrets.ZED_AMPLITUDE_API_KEY }} ${{ secrets.ZED_AMPLITUDE_SECRET_KEY }}
\ No newline at end of file
+ - run: python script/amplitude_release/main.py ${{ github.event.release.tag_name }} ${{ secrets.ZED_AMPLITUDE_API_KEY }} ${{ secrets.ZED_AMPLITUDE_SECRET_KEY }}
diff --git a/.gitignore b/.gitignore
index 2d721f8ad2bcaeb943a590320ebb193653ce91e4..93079fad52b245b8a1d85b087d255a4119d11c98 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,6 @@
/crates/collab/static/styles.css
/vendor/bin
/assets/themes/*.json
-/assets/themes/internal/*.json
-/assets/themes/experiments/*.json
+/assets/themes/Internal/*.json
+/assets/themes/Experiments/*.json
**/venv
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000000000000000000000000000000000..b1dad4cbbe1f31bdefe920092b22612c3e6f6487
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "crates/live_kit_server/protocol"]
+ path = crates/live_kit_server/protocol
+ url = https://github.com/livekit/protocol
diff --git a/Cargo.lock b/Cargo.lock
index d960b467ad6e7b2b14ecb8017118374cf9c733cb..65b562c8edbf903a80d078f6d1e4c1c68e0b01ab 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -172,13 +172,13 @@ dependencies = [
[[package]]
name = "async-broadcast"
-version = "0.3.4"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b"
+checksum = "6d26004fe83b2d1cd3a97609b21e39f9a31535822210fe83205d2ce48866ea61"
dependencies = [
- "easy-parallel",
"event-listener",
"futures-core",
+ "parking_lot 0.12.1",
]
[[package]]
@@ -716,10 +716,13 @@ name = "call"
version = "0.1.0"
dependencies = [
"anyhow",
+ "async-broadcast",
"client",
"collections",
"futures 0.3.24",
"gpui",
+ "live_kit_client",
+ "media",
"postage",
"project",
"util",
@@ -791,34 +794,6 @@ dependencies = [
"winx",
]
-[[package]]
-name = "capture"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "bindgen",
- "block",
- "byteorder",
- "bytes 1.2.1",
- "cocoa",
- "core-foundation",
- "core-graphics",
- "foreign-types",
- "futures 0.3.24",
- "gpui",
- "hmac 0.12.1",
- "jwt",
- "live_kit",
- "log",
- "media",
- "objc",
- "parking_lot 0.11.2",
- "postage",
- "serde",
- "sha2 0.10.6",
- "simplelog",
-]
-
[[package]]
name = "castaway"
version = "0.1.2"
@@ -1076,6 +1051,8 @@ dependencies = [
"language",
"lazy_static",
"lipsum",
+ "live_kit_client",
+ "live_kit_server",
"log",
"lsp",
"nanoid",
@@ -3165,17 +3142,54 @@ dependencies = [
]
[[package]]
-name = "live_kit"
+name = "live_kit_client"
version = "0.1.0"
dependencies = [
"anyhow",
+ "async-broadcast",
+ "async-trait",
+ "block",
+ "byteorder",
+ "bytes 1.2.1",
+ "cocoa",
+ "collections",
"core-foundation",
"core-graphics",
+ "foreign-types",
"futures 0.3.24",
+ "gpui",
+ "hmac 0.12.1",
+ "jwt",
+ "lazy_static",
+ "live_kit_server",
+ "log",
"media",
+ "nanoid",
+ "objc",
"parking_lot 0.11.2",
+ "postage",
"serde",
"serde_json",
+ "sha2 0.10.6",
+ "simplelog",
+]
+
+[[package]]
+name = "live_kit_server"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "futures 0.3.24",
+ "hmac 0.12.1",
+ "jwt",
+ "log",
+ "prost 0.8.0",
+ "prost-build",
+ "prost-types 0.8.0",
+ "reqwest",
+ "serde",
+ "sha2 0.10.6",
]
[[package]]
@@ -4332,7 +4346,7 @@ dependencies = [
"multimap",
"petgraph",
"prost 0.9.0",
- "prost-types",
+ "prost-types 0.9.0",
"regex",
"tempfile",
"which",
@@ -4364,6 +4378,16 @@ dependencies = [
"syn",
]
+[[package]]
+name = "prost-types"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "603bbd6394701d13f3f25aada59c7de9d35a6a5887cfc156181234a44002771b"
+dependencies = [
+ "bytes 1.2.1",
+ "prost 0.8.0",
+]
+
[[package]]
name = "prost-types"
version = "0.9.0"
diff --git a/assets/icons/disable_screen_sharing_12.svg b/assets/icons/disable_screen_sharing_12.svg
new file mode 100644
index 0000000000000000000000000000000000000000..c2a4edd45b26b530c16b8c68e612e620e493ac4f
--- /dev/null
+++ b/assets/icons/disable_screen_sharing_12.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/assets/icons/enable_screen_sharing_12.svg b/assets/icons/enable_screen_sharing_12.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6ae37649d29997107b3ddd42350b6333556a95cf
--- /dev/null
+++ b/assets/icons/enable_screen_sharing_12.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/assets/settings/default.json b/assets/settings/default.json
index 51aa108cd98a7bd5d16af5882b4293cca112239c..3eb82e94c7e13c99fc7a420a1053dc02d156980a 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -1,230 +1,230 @@
{
- // The name of the Zed theme to use for the UI
- "theme": "one-dark",
- // The name of a font to use for rendering text in the editor
- "buffer_font_family": "Zed Mono",
- // The default font size for text in the editor
- "buffer_font_size": 15,
- // Whether to enable vim modes and key bindings
- "vim_mode": false,
- // Whether to show the informational hover box when moving the mouse
- // over symbols in the editor.
- "hover_popover_enabled": true,
- // Whether the cursor blinks in the editor.
- "cursor_blink": true,
- // Whether to pop the completions menu while typing in an editor without
- // explicitly requesting it.
- "show_completions_on_input": true,
- // Whether new projects should start out 'online'. Online projects
- // appear in the contacts panel under your name, so that your contacts
- // can see which projects you are working on. Regardless of this
- // setting, projects keep their last online status when you reopen them.
- "projects_online_by_default": true,
- // Whether to use language servers to provide code intelligence.
- "enable_language_server": true,
- // When to automatically save edited buffers. This setting can
- // take four values.
- //
- // 1. Never automatically save:
- // "autosave": "off",
- // 2. Save when changing focus away from the Zed window:
- // "autosave": "on_window_change",
- // 3. Save when changing focus away from a specific buffer:
- // "autosave": "on_focus_change",
- // 4. Save when idle for a certain amount of time:
- // "autosave": { "after_delay": {"milliseconds": 500} },
- "autosave": "off",
- // Where to place the dock by default. This setting can take three
- // values:
- //
- // 1. Position the dock attached to the bottom of the workspace
- // "default_dock_anchor": "bottom"
- // 2. Position the dock to the right of the workspace like a side panel
- // "default_dock_anchor": "right"
- // 3. Position the dock full screen over the entire workspace"
- // "default_dock_anchor": "expanded"
- "default_dock_anchor": "right",
- // Whether or not to perform a buffer format before saving
- "format_on_save": "on",
- // How to perform a buffer format. This setting can take two values:
- //
- // 1. Format code using the current language server:
- // "format_on_save": "language_server"
- // 2. Format code using an external command:
- // "format_on_save": {
- // "external": {
- // "command": "prettier",
- // "arguments": ["--stdin-filepath", "{buffer_path}"]
- // }
+ // The name of the Zed theme to use for the UI
+ "theme": "One Dark",
+ // The name of a font to use for rendering text in the editor
+ "buffer_font_family": "Zed Mono",
+ // The default font size for text in the editor
+ "buffer_font_size": 15,
+ // Whether to enable vim modes and key bindings
+ "vim_mode": false,
+ // Whether to show the informational hover box when moving the mouse
+ // over symbols in the editor.
+ "hover_popover_enabled": true,
+ // Whether the cursor blinks in the editor.
+ "cursor_blink": true,
+ // Whether to pop the completions menu while typing in an editor without
+ // explicitly requesting it.
+ "show_completions_on_input": true,
+ // Whether new projects should start out 'online'. Online projects
+ // appear in the contacts panel under your name, so that your contacts
+ // can see which projects you are working on. Regardless of this
+ // setting, projects keep their last online status when you reopen them.
+ "projects_online_by_default": true,
+ // Whether to use language servers to provide code intelligence.
+ "enable_language_server": true,
+ // When to automatically save edited buffers. This setting can
+ // take four values.
+ //
+ // 1. Never automatically save:
+ // "autosave": "off",
+ // 2. Save when changing focus away from the Zed window:
+ // "autosave": "on_window_change",
+ // 3. Save when changing focus away from a specific buffer:
+ // "autosave": "on_focus_change",
+ // 4. Save when idle for a certain amount of time:
+ // "autosave": { "after_delay": {"milliseconds": 500} },
+ "autosave": "off",
+ // Where to place the dock by default. This setting can take three
+ // values:
+ //
+ // 1. Position the dock attached to the bottom of the workspace
+ // "default_dock_anchor": "bottom"
+ // 2. Position the dock to the right of the workspace like a side panel
+ // "default_dock_anchor": "right"
+ // 3. Position the dock full screen over the entire workspace"
+ // "default_dock_anchor": "expanded"
+ "default_dock_anchor": "right",
+ // Whether or not to perform a buffer format before saving
+ "format_on_save": "on",
+ // How to perform a buffer format. This setting can take two values:
+ //
+ // 1. Format code using the current language server:
+ // "format_on_save": "language_server"
+ // 2. Format code using an external command:
+ // "format_on_save": {
+ // "external": {
+ // "command": "prettier",
+ // "arguments": ["--stdin-filepath", "{buffer_path}"]
+ // }
+ // }
+ "formatter": "language_server",
+ // How to soft-wrap long lines of text. This setting can take
+ // three values:
+ //
+ // 1. Do not soft wrap.
+ // "soft_wrap": "none",
+ // 2. Soft wrap lines that overflow the editor:
+ // "soft_wrap": "editor_width",
+ // 3. Soft wrap lines at the preferred line length
+ // "soft_wrap": "preferred_line_length",
+ "soft_wrap": "none",
+ // The column at which to soft-wrap lines, for buffers where soft-wrap
+ // is enabled.
+ "preferred_line_length": 80,
+ // Whether to indent lines using tab characters, as opposed to multiple
+ // spaces.
+ "hard_tabs": false,
+ // How many columns a tab should occupy.
+ "tab_size": 4,
+ // Git gutter behavior configuration.
+ "git": {
+ // Control whether the git gutter is shown. May take 2 values:
+ // 1. Show the gutter
+ // "git_gutter": "tracked_files"
+ // 2. Hide the gutter
+ // "git_gutter": "hide"
+ "git_gutter": "tracked_files"
+ },
+ // Settings specific to journaling
+ "journal": {
+ // The path of the directory where journal entries are stored
+ "path": "~",
+ // What format to display the hours in
+ // May take 2 values:
+ // 1. hour12
+ // 2. hour24
+ "hour_format": "hour12"
+ },
+ // Settings specific to the terminal
+ "terminal": {
+ // What shell to use when opening a terminal. May take 3 values:
+ // 1. Use the system's default terminal configuration (e.g. $TERM).
+ // "shell": "system"
+ // 2. A program:
+ // "shell": {
+ // "program": "sh"
+ // }
+ // 3. A program with arguments:
+ // "shell": {
+ // "with_arguments": {
+ // "program": "/bin/bash",
+ // "arguments": ["--login"]
+ // }
// }
- "formatter": "language_server",
- // How to soft-wrap long lines of text. This setting can take
- // three values:
+ "shell": "system",
+ // What working directory to use when launching the terminal.
+ // May take 4 values:
+ // 1. Use the current file's project directory. Will Fallback to the
+ // first project directory strategy if unsuccessful
+ // "working_directory": "current_project_directory"
+ // 2. Use the first project in this workspace's directory
+ // "working_directory": "first_project_directory"
+ // 3. Always use this platform's home directory (if we can find it)
+ // "working_directory": "always_home"
+ // 4. Always use a specific directory. This value will be shell expanded.
+ // If this path is not a valid directory the terminal will default to
+ // this platform's home directory (if we can find it)
+ // "working_directory": {
+ // "always": {
+ // "directory": "~/zed/projects/"
+ // }
+ // }
//
- // 1. Do not soft wrap.
- // "soft_wrap": "none",
- // 2. Soft wrap lines that overflow the editor:
- // "soft_wrap": "editor_width",
- // 3. Soft wrap lines at the preferred line length
- // "soft_wrap": "preferred_line_length",
- "soft_wrap": "none",
- // The column at which to soft-wrap lines, for buffers where soft-wrap
- // is enabled.
- "preferred_line_length": 80,
- // Whether to indent lines using tab characters, as opposed to multiple
- // spaces.
- "hard_tabs": false,
- // How many columns a tab should occupy.
- "tab_size": 4,
- // Git gutter behavior configuration.
- "git": {
- // Control whether the git gutter is shown. May take 2 values:
- // 1. Show the gutter
- // "git_gutter": "tracked_files"
- // 2. Hide the gutter
- // "git_gutter": "hide"
- "git_gutter": "tracked_files"
+ //
+ "working_directory": "current_project_directory",
+ // Set the cursor blinking behavior in the terminal.
+ // May take 4 values:
+ // 1. Never blink the cursor, ignoring the terminal mode
+ // "blinking": "off",
+ // 2. Default the cursor blink to off, but allow the terminal to
+ // set blinking
+ // "blinking": "terminal_controlled",
+ // 3. Always blink the cursor, ignoring the terminal mode
+ // "blinking": "on",
+ "blinking": "terminal_controlled",
+ // Set whether Alternate Scroll mode (code: ?1007) is active by default.
+ // Alternate Scroll mode converts mouse scroll events into up / down key
+ // presses when in the alternate screen (e.g. when running applications
+ // like vim or less). The terminal can still set and unset this mode.
+ // May take 2 values:
+ // 1. Default alternate scroll mode to on
+ // "alternate_scroll": "on",
+ // 2. Default alternate scroll mode to off
+ // "alternate_scroll": "off",
+ "alternate_scroll": "off",
+ // Set whether the option key behaves as the meta key.
+ // May take 2 values:
+ // 1. Rely on default platform handling of option key, on macOS
+ // this means generating certain unicode characters
+ // "option_to_meta": false,
+ // 2. Make the option keys behave as a 'meta' key, e.g. for emacs
+ // "option_to_meta": true,
+ "option_as_meta": false,
+ // Whether or not selecting text in the terminal will automatically
+ // copy to the system clipboard.
+ "copy_on_select": false,
+ // Any key-value pairs added to this list will be added to the terminal's
+ // enviroment. Use `:` to seperate multiple values.
+ "env": {
+ // "KEY": "value1:value2"
+ }
+ // Set the terminal's font size. If this option is not included,
+ // the terminal will default to matching the buffer's font size.
+ // "font_size": "15"
+ // Set the terminal's font family. If this option is not included,
+ // the terminal will default to matching the buffer's font family.
+ // "font_family": "Zed Mono"
+ },
+ // Different settings for specific languages.
+ "languages": {
+ "Plain Text": {
+ "soft_wrap": "preferred_line_length"
+ },
+ "C": {
+ "tab_size": 2
},
- // Settings specific to journaling
- "journal": {
- // The path of the directory where journal entries are stored
- "path": "~",
- // What format to display the hours in
- // May take 2 values:
- // 1. hour12
- // 2. hour24
- "hour_format": "hour12"
+ "C++": {
+ "tab_size": 2
},
- // Settings specific to the terminal
- "terminal": {
- // What shell to use when opening a terminal. May take 3 values:
- // 1. Use the system's default terminal configuration (e.g. $TERM).
- // "shell": "system"
- // 2. A program:
- // "shell": {
- // "program": "sh"
- // }
- // 3. A program with arguments:
- // "shell": {
- // "with_arguments": {
- // "program": "/bin/bash",
- // "arguments": ["--login"]
- // }
- // }
- "shell": "system",
- // What working directory to use when launching the terminal.
- // May take 4 values:
- // 1. Use the current file's project directory. Will Fallback to the
- // first project directory strategy if unsuccessful
- // "working_directory": "current_project_directory"
- // 2. Use the first project in this workspace's directory
- // "working_directory": "first_project_directory"
- // 3. Always use this platform's home directory (if we can find it)
- // "working_directory": "always_home"
- // 4. Always use a specific directory. This value will be shell expanded.
- // If this path is not a valid directory the terminal will default to
- // this platform's home directory (if we can find it)
- // "working_directory": {
- // "always": {
- // "directory": "~/zed/projects/"
- // }
- // }
- //
- //
- "working_directory": "current_project_directory",
- // Set the cursor blinking behavior in the terminal.
- // May take 4 values:
- // 1. Never blink the cursor, ignoring the terminal mode
- // "blinking": "off",
- // 2. Default the cursor blink to off, but allow the terminal to
- // set blinking
- // "blinking": "terminal_controlled",
- // 3. Always blink the cursor, ignoring the terminal mode
- // "blinking": "on",
- "blinking": "terminal_controlled",
- // Set whether Alternate Scroll mode (code: ?1007) is active by default.
- // Alternate Scroll mode converts mouse scroll events into up / down key
- // presses when in the alternate screen (e.g. when running applications
- // like vim or less). The terminal can still set and unset this mode.
- // May take 2 values:
- // 1. Default alternate scroll mode to on
- // "alternate_scroll": "on",
- // 2. Default alternate scroll mode to off
- // "alternate_scroll": "off",
- "alternate_scroll": "off",
- // Set whether the option key behaves as the meta key.
- // May take 2 values:
- // 1. Rely on default platform handling of option key, on macOS
- // this means generating certain unicode characters
- // "option_to_meta": false,
- // 2. Make the option keys behave as a 'meta' key, e.g. for emacs
- // "option_to_meta": true,
- "option_as_meta": false,
- // Whether or not selecting text in the terminal will automatically
- // copy to the system clipboard.
- "copy_on_select": false,
- // Any key-value pairs added to this list will be added to the terminal's
- // enviroment. Use `:` to seperate multiple values.
- "env": {
- // "KEY": "value1:value2"
- }
- // Set the terminal's font size. If this option is not included,
- // the terminal will default to matching the buffer's font size.
- // "font_size": "15"
- // Set the terminal's font family. If this option is not included,
- // the terminal will default to matching the buffer's font family.
- // "font_family": "Zed Mono"
+ "Elixir": {
+ "tab_size": 2
},
- // Different settings for specific languages.
- "languages": {
- "Plain Text": {
- "soft_wrap": "preferred_line_length"
- },
- "C": {
- "tab_size": 2
- },
- "C++": {
- "tab_size": 2
- },
- "Elixir": {
- "tab_size": 2
- },
- "Go": {
- "tab_size": 4,
- "hard_tabs": true
- },
- "Markdown": {
- "soft_wrap": "preferred_line_length"
- },
- "Rust": {
- "tab_size": 4
- },
- "JavaScript": {
- "tab_size": 2
- },
- "TypeScript": {
- "tab_size": 2
- },
- "TSX": {
- "tab_size": 2
- }
+ "Go": {
+ "tab_size": 4,
+ "hard_tabs": true
},
- // LSP Specific settings.
- "lsp": {
- // Specify the LSP name as a key here.
- // As of 8/10/22, supported LSPs are:
- // pyright
- // gopls
- // rust-analyzer
- // typescript-language-server
- // vscode-json-languageserver
- // "rust_analyzer": {
- // //These initialization options are merged into Zed's defaults
- // "initialization_options": {
- // "checkOnSave": {
- // "command": "clippy"
- // }
- // }
- // }
+ "Markdown": {
+ "soft_wrap": "preferred_line_length"
+ },
+ "Rust": {
+ "tab_size": 4
+ },
+ "JavaScript": {
+ "tab_size": 2
+ },
+ "TypeScript": {
+ "tab_size": 2
+ },
+ "TSX": {
+ "tab_size": 2
}
+ },
+ // LSP Specific settings.
+ "lsp": {
+ // Specify the LSP name as a key here.
+ // As of 8/10/22, supported LSPs are:
+ // pyright
+ // gopls
+ // rust-analyzer
+ // typescript-language-server
+ // vscode-json-languageserver
+ // "rust_analyzer": {
+ // //These initialization options are merged into Zed's defaults
+ // "initialization_options": {
+ // "checkOnSave": {
+ // "command": "clippy"
+ // }
+ // }
+ // }
+ }
}
diff --git a/assets/themes/experiments/.gitkeep b/assets/themes/experiments/.gitkeep
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/assets/themes/internal/.gitkeep b/assets/themes/internal/.gitkeep
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml
index e725c7cfe3b053d36f1b04b81d1d5476e68e7bed..a7a3331d20be6b93701999de33d0922e184e38af 100644
--- a/crates/call/Cargo.toml
+++ b/crates/call/Cargo.toml
@@ -12,6 +12,7 @@ test-support = [
"client/test-support",
"collections/test-support",
"gpui/test-support",
+ "live_kit_client/test-support",
"project/test-support",
"util/test-support"
]
@@ -20,10 +21,13 @@ test-support = [
client = { path = "../client" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
+live_kit_client = { path = "../live_kit_client" }
+media = { path = "../media" }
project = { path = "../project" }
util = { path = "../util" }
anyhow = "1.0.38"
+async-broadcast = "0.4"
futures = "0.3"
postage = { version = "0.4.1", features = ["futures-traits"] }
@@ -31,5 +35,6 @@ postage = { version = "0.4.1", features = ["futures-traits"] }
client = { path = "../client", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
+live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs
index 6b06d04375b8a1fca563ef3be558f823ce45cd1c..106006007c76cdf83ea18a8442aa9845757ed5b2 100644
--- a/crates/call/src/call.rs
+++ b/crates/call/src/call.rs
@@ -1,11 +1,11 @@
-mod participant;
+pub mod participant;
pub mod room;
use anyhow::{anyhow, Result};
use client::{proto, Client, TypedEnvelope, User, UserStore};
use gpui::{
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
- Subscription, Task,
+ Subscription, Task, WeakModelHandle,
};
pub use participant::ParticipantLocation;
use postage::watch;
@@ -27,6 +27,7 @@ pub struct IncomingCall {
}
pub struct ActiveCall {
+ location: Option>,
room: Option<(ModelHandle, Vec)>,
incoming_call: (
watch::Sender>,
@@ -49,6 +50,7 @@ impl ActiveCall {
) -> Self {
Self {
room: None,
+ location: None,
incoming_call: watch::channel(),
_subscriptions: vec![
client.add_request_handler(cx.handle(), Self::handle_incoming_call),
@@ -132,7 +134,9 @@ impl ActiveCall {
Room::create(recipient_user_id, initial_project, client, user_store, cx)
})
.await?;
- this.update(&mut cx, |this, cx| this.set_room(Some(room), cx));
+
+ this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))
+ .await?;
};
Ok(())
@@ -180,7 +184,8 @@ impl ActiveCall {
let join = Room::join(&call, self.client.clone(), self.user_store.clone(), cx);
cx.spawn(|this, mut cx| async move {
let room = join.await?;
- this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx));
+ this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))
+ .await?;
Ok(())
})
}
@@ -223,35 +228,46 @@ impl ActiveCall {
project: Option<&ModelHandle>,
cx: &mut ModelContext,
) -> Task> {
+ self.location = project.map(|project| project.downgrade());
if let Some((room, _)) = self.room.as_ref() {
room.update(cx, |room, cx| room.set_location(project, cx))
} else {
- Task::ready(Err(anyhow!("no active call")))
+ Task::ready(Ok(()))
}
}
- fn set_room(&mut self, room: Option>, cx: &mut ModelContext) {
+ fn set_room(
+ &mut self,
+ room: Option>,
+ cx: &mut ModelContext,
+ ) -> Task> {
if room.as_ref() != self.room.as_ref().map(|room| &room.0) {
+ cx.notify();
if let Some(room) = room {
if room.read(cx).status().is_offline() {
self.room = None;
+ Task::ready(Ok(()))
} else {
let subscriptions = vec![
cx.observe(&room, |this, room, cx| {
if room.read(cx).status().is_offline() {
- this.set_room(None, cx);
+ this.set_room(None, cx).detach_and_log_err(cx);
}
cx.notify();
}),
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
];
- self.room = Some((room, subscriptions));
+ self.room = Some((room.clone(), subscriptions));
+ let location = self.location.and_then(|location| location.upgrade(cx));
+ room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
}
} else {
self.room = None;
+ Task::ready(Ok(()))
}
- cx.notify();
+ } else {
+ Task::ready(Ok(()))
}
}
diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs
index a5be5b4af2779a369f0e0707b8144c873a6e61d5..dfa456f7345d06a154e64e0d013abe8205405d0c 100644
--- a/crates/call/src/participant.rs
+++ b/crates/call/src/participant.rs
@@ -1,6 +1,8 @@
use anyhow::{anyhow, Result};
use client::{proto, User};
+use collections::HashMap;
use gpui::WeakModelHandle;
+pub use live_kit_client::Frame;
use project::Project;
use std::sync::Arc;
@@ -34,9 +36,21 @@ pub struct LocalParticipant {
pub active_project: Option>,
}
-#[derive(Clone, Debug)]
+#[derive(Clone)]
pub struct RemoteParticipant {
pub user: Arc,
pub projects: Vec,
pub location: ParticipantLocation,
+ pub tracks: HashMap>,
+}
+
+#[derive(Clone)]
+pub struct RemoteVideoTrack {
+ pub(crate) live_kit_track: Arc,
+}
+
+impl RemoteVideoTrack {
+ pub fn frames(&self) -> async_broadcast::Receiver {
+ self.live_kit_track.frames()
+ }
}
diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs
index b2e79f820d3109589a38a1d7baf0ec64bcdce027..7d5153950d76d16f3e3185835eed22ac430fda97 100644
--- a/crates/call/src/room.rs
+++ b/crates/call/src/room.rs
@@ -1,5 +1,5 @@
use crate::{
- participant::{LocalParticipant, ParticipantLocation, RemoteParticipant},
+ participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack},
IncomingCall,
};
use anyhow::{anyhow, Result};
@@ -7,12 +7,20 @@ use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
use collections::{BTreeMap, HashSet};
use futures::StreamExt;
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
+use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate};
+use postage::stream::Stream;
use project::Project;
-use std::{os::unix::prelude::OsStrExt, sync::Arc};
-use util::ResultExt;
+use std::{mem, os::unix::prelude::OsStrExt, sync::Arc};
+use util::{post_inc, ResultExt};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
+ ParticipantLocationChanged {
+ participant_id: PeerId,
+ },
+ RemoteVideoTracksChanged {
+ participant_id: PeerId,
+ },
RemoteProjectShared {
owner: Arc,
project_id: u64,
@@ -26,6 +34,7 @@ pub enum Event {
pub struct Room {
id: u64,
+ live_kit: Option,
status: RoomStatus,
local_participant: LocalParticipant,
remote_participants: BTreeMap,
@@ -43,13 +52,16 @@ impl Entity for Room {
type Event = Event;
fn release(&mut self, _: &mut MutableAppContext) {
- self.client.send(proto::LeaveRoom { id: self.id }).log_err();
+ if self.status.is_online() {
+ self.client.send(proto::LeaveRoom { id: self.id }).log_err();
+ }
}
}
impl Room {
fn new(
id: u64,
+ live_kit_connection_info: Option,
client: Arc,
user_store: ModelHandle,
cx: &mut ModelContext,
@@ -69,8 +81,59 @@ impl Room {
})
.detach();
+ 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_weak(|this, mut cx| async move {
+ while let Some(status) = status.next().await {
+ let this = if let Some(this) = this.upgrade(&cx) {
+ this
+ } else {
+ break;
+ };
+
+ if status == live_kit_client::ConnectionState::Disconnected {
+ this.update(&mut cx, |this, cx| this.leave(cx).log_err());
+ break;
+ }
+ }
+ });
+
+ let mut track_changes = room.remote_video_track_updates();
+ let _maintain_tracks = cx.spawn_weak(|this, mut cx| async move {
+ while let Some(track_change) = track_changes.next().await {
+ let this = if let Some(this) = this.upgrade(&cx) {
+ this
+ } else {
+ break;
+ };
+
+ this.update(&mut cx, |this, cx| {
+ this.remote_video_track_updated(track_change, cx).log_err()
+ });
+ }
+ });
+
+ cx.foreground()
+ .spawn(room.connect(&connection_info.server_url, &connection_info.token))
+ .detach_and_log_err(cx);
+
+ Some(LiveKitRoom {
+ room,
+ screen_track: ScreenTrack::None,
+ next_publish_id: 0,
+ _maintain_room,
+ _maintain_tracks,
+ })
+ } else {
+ None
+ };
+
Self {
id,
+ live_kit: live_kit_room,
status: RoomStatus::Online,
participant_user_ids: Default::default(),
local_participant: Default::default(),
@@ -94,7 +157,16 @@ impl Room {
) -> Task>> {
cx.spawn(|mut cx| async move {
let response = client.request(proto::CreateRoom {}).await?;
- let room = cx.add_model(|cx| Self::new(response.id, client, user_store, cx));
+ let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
+ let room = cx.add_model(|cx| {
+ Self::new(
+ room_proto.id,
+ response.live_kit_connection_info,
+ client,
+ user_store,
+ cx,
+ )
+ });
let initial_project_id = if let Some(initial_project) = initial_project {
let initial_project_id = room
@@ -130,7 +202,15 @@ impl Room {
cx.spawn(|mut cx| async move {
let response = client.request(proto::JoinRoom { id: room_id }).await?;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
- let room = cx.add_model(|cx| Self::new(room_id, client, user_store, cx));
+ let room = cx.add_model(|cx| {
+ Self::new(
+ room_id,
+ response.live_kit_connection_info,
+ client,
+ user_store,
+ cx,
+ )
+ });
room.update(&mut cx, |room, cx| {
room.leave_when_empty = true;
room.apply_room_update(room_proto, cx)?;
@@ -160,6 +240,7 @@ impl Room {
self.pending_participants.clear();
self.participant_user_ids.clear();
self.subscriptions.clear();
+ self.live_kit.take();
self.client.send(proto::LeaveRoom { id: self.id })?;
Ok(())
}
@@ -272,15 +353,40 @@ impl Room {
});
}
- this.remote_participants.insert(
- peer_id,
- RemoteParticipant {
- user: user.clone(),
- projects: participant.projects,
- location: ParticipantLocation::from_proto(participant.location)
- .unwrap_or(ParticipantLocation::External),
- },
- );
+ let location = ParticipantLocation::from_proto(participant.location)
+ .unwrap_or(ParticipantLocation::External);
+ if let Some(remote_participant) = this.remote_participants.get_mut(&peer_id)
+ {
+ remote_participant.projects = participant.projects;
+ if location != remote_participant.location {
+ remote_participant.location = location;
+ cx.emit(Event::ParticipantLocationChanged {
+ participant_id: peer_id,
+ });
+ }
+ } else {
+ this.remote_participants.insert(
+ peer_id,
+ RemoteParticipant {
+ user: user.clone(),
+ projects: participant.projects,
+ location,
+ tracks: Default::default(),
+ },
+ );
+
+ if let Some(live_kit) = this.live_kit.as_ref() {
+ let tracks =
+ live_kit.room.remote_video_tracks(&peer_id.0.to_string());
+ for track in tracks {
+ this.remote_video_track_updated(
+ RemoteVideoTrackUpdate::Subscribed(track),
+ cx,
+ )
+ .log_err();
+ }
+ }
+ }
}
this.remote_participants.retain(|_, participant| {
@@ -318,6 +424,49 @@ impl Room {
Ok(())
}
+ fn remote_video_track_updated(
+ &mut self,
+ change: RemoteVideoTrackUpdate,
+ cx: &mut ModelContext,
+ ) -> Result<()> {
+ match change {
+ RemoteVideoTrackUpdate::Subscribed(track) => {
+ let peer_id = PeerId(track.publisher_id().parse()?);
+ let track_id = track.sid().to_string();
+ let participant = self
+ .remote_participants
+ .get_mut(&peer_id)
+ .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
+ participant.tracks.insert(
+ track_id.clone(),
+ Arc::new(RemoteVideoTrack {
+ live_kit_track: track,
+ }),
+ );
+ cx.emit(Event::RemoteVideoTracksChanged {
+ participant_id: peer_id,
+ });
+ }
+ RemoteVideoTrackUpdate::Unsubscribed {
+ publisher_id,
+ track_id,
+ } => {
+ let peer_id = PeerId(publisher_id.parse()?);
+ let participant = self
+ .remote_participants
+ .get_mut(&peer_id)
+ .ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
+ participant.tracks.remove(&track_id);
+ cx.emit(Event::RemoteVideoTracksChanged {
+ participant_id: peer_id,
+ });
+ }
+ }
+
+ cx.notify();
+ Ok(())
+ }
+
fn check_invariants(&self) {
#[cfg(any(test, feature = "test-support"))]
{
@@ -418,7 +567,7 @@ impl Room {
})
}
- pub fn set_location(
+ pub(crate) fn set_location(
&mut self,
project: Option<&ModelHandle>,
cx: &mut ModelContext,
@@ -458,6 +607,140 @@ impl Room {
Ok(())
})
}
+
+ pub fn is_screen_sharing(&self) -> bool {
+ self.live_kit.as_ref().map_or(false, |live_kit| {
+ !matches!(live_kit.screen_track, ScreenTrack::None)
+ })
+ }
+
+ pub fn share_screen(&mut self, cx: &mut ModelContext) -> Task> {
+ if self.status.is_offline() {
+ return Task::ready(Err(anyhow!("room is offline")));
+ } else if self.is_screen_sharing() {
+ return Task::ready(Err(anyhow!("screen was already shared")));
+ }
+
+ 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 = ScreenTrack::Pending { publish_id };
+ cx.notify();
+ (live_kit.room.display_sources(), publish_id)
+ } else {
+ return Task::ready(Err(anyhow!("live-kit was not initialized")));
+ };
+
+ cx.spawn_weak(|this, mut cx| async move {
+ 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(&cx)
+ .ok_or_else(|| anyhow!("room was dropped"))?
+ .read_with(&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
+ };
+
+ let publication = publish_track.await;
+ this.upgrade(&cx)
+ .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 ScreenTrack::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 = ScreenTrack::Published(publication);
+ cx.notify();
+ }
+ Ok(())
+ }
+ Err(error) => {
+ if canceled {
+ Ok(())
+ } else {
+ live_kit.screen_track = ScreenTrack::None;
+ cx.notify();
+ Err(error)
+ }
+ }
+ }
+ })
+ })
+ }
+
+ pub fn unshare_screen(&mut self, cx: &mut ModelContext) -> Result<()> {
+ if self.status.is_offline() {
+ return Err(anyhow!("room is offline"));
+ }
+
+ let live_kit = self
+ .live_kit
+ .as_mut()
+ .ok_or_else(|| anyhow!("live-kit was not initialized"))?;
+ match mem::take(&mut live_kit.screen_track) {
+ ScreenTrack::None => Err(anyhow!("screen was not shared")),
+ ScreenTrack::Pending { .. } => {
+ cx.notify();
+ Ok(())
+ }
+ ScreenTrack::Published(track) => {
+ live_kit.room.unpublish_track(track);
+ cx.notify();
+ Ok(())
+ }
+ }
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn set_display_sources(&self, sources: Vec) {
+ self.live_kit
+ .as_ref()
+ .unwrap()
+ .room
+ .set_display_sources(sources);
+ }
+}
+
+struct LiveKitRoom {
+ room: Arc,
+ screen_track: ScreenTrack,
+ next_publish_id: usize,
+ _maintain_room: Task<()>,
+ _maintain_tracks: Task<()>,
+}
+
+enum ScreenTrack {
+ None,
+ Pending { publish_id: usize },
+ Published(LocalTrackPublication),
+}
+
+impl Default for ScreenTrack {
+ fn default() -> Self {
+ Self::None
+ }
}
#[derive(Copy, Clone, PartialEq, Eq)]
@@ -470,4 +753,8 @@ impl RoomStatus {
pub fn is_offline(&self) -> bool {
matches!(self, RoomStatus::Offline)
}
+
+ pub fn is_online(&self) -> bool {
+ matches!(self, RoomStatus::Online)
+ }
}
diff --git a/crates/capture/Cargo.toml b/crates/capture/Cargo.toml
deleted file mode 100644
index f8ed31097a81c8e96430c89df7b2a64b8e93a767..0000000000000000000000000000000000000000
--- a/crates/capture/Cargo.toml
+++ /dev/null
@@ -1,32 +0,0 @@
-[package]
-name = "capture"
-version = "0.1.0"
-edition = "2021"
-description = "An example of screen capture"
-
-[dependencies]
-gpui = { path = "../gpui" }
-live_kit = { path = "../live_kit" }
-media = { path = "../media" }
-
-anyhow = "1.0.38"
-block = "0.1"
-bytes = "1.2"
-byteorder = "1.4"
-cocoa = "0.24"
-core-foundation = "0.9.3"
-core-graphics = "0.22.3"
-foreign-types = "0.3"
-futures = "0.3"
-hmac = "0.12"
-jwt = "0.16"
-log = { version = "0.4.16", features = ["kv_unstable_serde"] }
-objc = "0.2"
-parking_lot = "0.11.1"
-postage = { version = "0.4.1", features = ["futures-traits"] }
-serde = { version = "1.0", features = ["derive", "rc"] }
-sha2 = "0.10"
-simplelog = "0.9"
-
-[build-dependencies]
-bindgen = "0.59.2"
diff --git a/crates/capture/build.rs b/crates/capture/build.rs
deleted file mode 100644
index 41f60ff48611e0b2e174e46de5455ae5516ed15f..0000000000000000000000000000000000000000
--- a/crates/capture/build.rs
+++ /dev/null
@@ -1,7 +0,0 @@
-fn main() {
- // Find WebRTC.framework as a sibling of the executable when running outside of an application bundle
- println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
-
- // Register exported Objective-C selectors, protocols, etc
- println!("cargo:rustc-link-arg=-Wl,-ObjC");
-}
diff --git a/crates/capture/src/live_kit_token.rs b/crates/capture/src/live_kit_token.rs
deleted file mode 100644
index be4fc4f4a2ade98d8ed4d76b6553cba13d5ab18e..0000000000000000000000000000000000000000
--- a/crates/capture/src/live_kit_token.rs
+++ /dev/null
@@ -1,71 +0,0 @@
-use anyhow::Result;
-use hmac::{Hmac, Mac};
-use jwt::SignWithKey;
-use serde::Serialize;
-use sha2::Sha256;
-use std::{
- ops::Add,
- time::{Duration, SystemTime, UNIX_EPOCH},
-};
-
-static DEFAULT_TTL: Duration = Duration::from_secs(6 * 60 * 60); // 6 hours
-
-#[derive(Default, Serialize)]
-#[serde(rename_all = "camelCase")]
-struct ClaimGrants<'a> {
- iss: &'a str,
- sub: &'a str,
- iat: u64,
- exp: u64,
- nbf: u64,
- jwtid: &'a str,
- video: VideoGrant<'a>,
-}
-
-#[derive(Default, Serialize)]
-#[serde(rename_all = "camelCase")]
-struct VideoGrant<'a> {
- room_create: Option,
- room_join: Option,
- room_list: Option,
- room_record: Option,
- room_admin: Option,
- room: Option<&'a str>,
- can_publish: Option,
- can_subscribe: Option,
- can_publish_data: Option,
- hidden: Option,
- recorder: Option,
-}
-
-pub fn create_token(
- api_key: &str,
- secret_key: &str,
- room_name: &str,
- participant_name: &str,
-) -> Result {
- let secret_key: Hmac = Hmac::new_from_slice(secret_key.as_bytes())?;
-
- let now = SystemTime::now();
-
- let claims = ClaimGrants {
- iss: api_key,
- sub: participant_name,
- iat: now.duration_since(UNIX_EPOCH).unwrap().as_secs(),
- exp: now
- .add(DEFAULT_TTL)
- .duration_since(UNIX_EPOCH)
- .unwrap()
- .as_secs(),
- nbf: 0,
- jwtid: participant_name,
- video: VideoGrant {
- room: Some(room_name),
- room_join: Some(true),
- can_publish: Some(true),
- can_subscribe: Some(true),
- ..Default::default()
- },
- };
- Ok(claims.sign_with_key(&secret_key)?)
-}
diff --git a/crates/capture/src/main.rs b/crates/capture/src/main.rs
deleted file mode 100644
index c34f451e41e14ae539259c231c05e2e6684e0408..0000000000000000000000000000000000000000
--- a/crates/capture/src/main.rs
+++ /dev/null
@@ -1,143 +0,0 @@
-mod live_kit_token;
-
-use futures::StreamExt;
-use gpui::{
- actions,
- elements::{Canvas, *},
- keymap::Binding,
- platform::current::Surface,
- Menu, MenuItem, ViewContext,
-};
-use live_kit::{LocalVideoTrack, Room};
-use log::LevelFilter;
-use media::core_video::CVImageBuffer;
-use postage::watch;
-use simplelog::SimpleLogger;
-use std::sync::Arc;
-
-actions!(capture, [Quit]);
-
-fn main() {
- SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
-
- gpui::App::new(()).unwrap().run(|cx| {
- cx.platform().activate(true);
- cx.add_global_action(quit);
-
- cx.add_bindings([Binding::new("cmd-q", Quit, None)]);
- cx.set_menus(vec![Menu {
- name: "Zed",
- items: vec![MenuItem::Action {
- name: "Quit",
- action: Box::new(Quit),
- }],
- }]);
-
- let live_kit_url = std::env::var("LIVE_KIT_URL").unwrap();
- let live_kit_key = std::env::var("LIVE_KIT_KEY").unwrap();
- let live_kit_secret = std::env::var("LIVE_KIT_SECRET").unwrap();
-
- cx.spawn(|mut cx| async move {
- let user1_token = live_kit_token::create_token(
- &live_kit_key,
- &live_kit_secret,
- "test-room",
- "test-participant-1",
- )
- .unwrap();
- let room1 = Room::new();
- room1.connect(&live_kit_url, &user1_token).await.unwrap();
-
- let user2_token = live_kit_token::create_token(
- &live_kit_key,
- &live_kit_secret,
- "test-room",
- "test-participant-2",
- )
- .unwrap();
- let room2 = Room::new();
- room2.connect(&live_kit_url, &user2_token).await.unwrap();
- cx.add_window(Default::default(), |cx| ScreenCaptureView::new(room2, cx));
-
- let windows = live_kit::list_windows();
- let window = windows
- .iter()
- .find(|w| w.owner_name.as_deref() == Some("Safari"))
- .unwrap();
- let track = LocalVideoTrack::screen_share_for_window(window.id);
- room1.publish_video_track(&track).await.unwrap();
- })
- .detach();
- });
-}
-
-struct ScreenCaptureView {
- image_buffer: Option,
- _room: Arc,
-}
-
-impl gpui::Entity for ScreenCaptureView {
- type Event = ();
-}
-
-impl ScreenCaptureView {
- pub fn new(room: Arc, cx: &mut ViewContext) -> Self {
- let mut remote_video_tracks = room.remote_video_tracks();
- cx.spawn_weak(|this, mut cx| async move {
- if let Some(video_track) = remote_video_tracks.next().await {
- let (mut frames_tx, mut frames_rx) = watch::channel_with(None);
- video_track.add_renderer(move |frame| *frames_tx.borrow_mut() = Some(frame));
-
- while let Some(frame) = frames_rx.next().await {
- if let Some(this) = this.upgrade(&cx) {
- this.update(&mut cx, |this, cx| {
- this.image_buffer = frame;
- cx.notify();
- });
- } else {
- break;
- }
- }
- }
- })
- .detach();
-
- Self {
- image_buffer: None,
- _room: room,
- }
- }
-}
-
-impl gpui::View for ScreenCaptureView {
- fn ui_name() -> &'static str {
- "View"
- }
-
- fn render(&mut self, _: &mut gpui::RenderContext) -> gpui::ElementBox {
- let image_buffer = self.image_buffer.clone();
- let canvas = Canvas::new(move |bounds, _, cx| {
- if let Some(image_buffer) = image_buffer.clone() {
- cx.scene.push_surface(Surface {
- bounds,
- image_buffer,
- });
- }
- });
-
- if let Some(image_buffer) = self.image_buffer.as_ref() {
- canvas
- .constrained()
- .with_width(image_buffer.width() as f32)
- .with_height(image_buffer.height() as f32)
- .aligned()
- .boxed()
- } else {
- canvas.boxed()
- }
- }
-}
-
-fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
- cx.platform().quit();
-}
diff --git a/crates/client/src/amplitude_telemetry.rs b/crates/client/src/amplitude_telemetry.rs
new file mode 100644
index 0000000000000000000000000000000000000000..5db2bedf03016b59fc81c54fa5d1cf0f999e5140
--- /dev/null
+++ b/crates/client/src/amplitude_telemetry.rs
@@ -0,0 +1,277 @@
+use crate::http::HttpClient;
+use db::Db;
+use gpui::{
+ executor::Background,
+ serde_json::{self, value::Map, Value},
+ AppContext, Task,
+};
+use isahc::Request;
+use lazy_static::lazy_static;
+use parking_lot::Mutex;
+use serde::Serialize;
+use serde_json::json;
+use std::{
+ io::Write,
+ mem,
+ path::PathBuf,
+ sync::Arc,
+ time::{Duration, SystemTime, UNIX_EPOCH},
+};
+use tempfile::NamedTempFile;
+use util::{post_inc, ResultExt, TryFutureExt};
+use uuid::Uuid;
+
+pub struct AmplitudeTelemetry {
+ http_client: Arc,
+ executor: Arc,
+ session_id: u128,
+ state: Mutex,
+}
+
+#[derive(Default)]
+struct AmplitudeTelemetryState {
+ metrics_id: Option>,
+ device_id: Option>,
+ app_version: Option>,
+ os_version: Option>,
+ os_name: &'static str,
+ queue: Vec,
+ next_event_id: usize,
+ flush_task: Option>,
+ log_file: Option,
+}
+
+const AMPLITUDE_EVENTS_URL: &'static str = "https://api2.amplitude.com/batch";
+
+lazy_static! {
+ static ref AMPLITUDE_API_KEY: Option = std::env::var("ZED_AMPLITUDE_API_KEY")
+ .ok()
+ .or_else(|| option_env!("ZED_AMPLITUDE_API_KEY").map(|key| key.to_string()));
+}
+
+#[derive(Serialize)]
+struct AmplitudeEventBatch {
+ api_key: &'static str,
+ events: Vec,
+}
+
+#[derive(Serialize)]
+struct AmplitudeEvent {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ user_id: Option>,
+ device_id: Option>,
+ event_type: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ event_properties: Option>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ user_properties: Option>,
+ os_name: &'static str,
+ os_version: Option>,
+ app_version: Option>,
+ platform: &'static str,
+ event_id: usize,
+ session_id: u128,
+ time: u128,
+}
+
+#[cfg(debug_assertions)]
+const MAX_QUEUE_LEN: usize = 1;
+
+#[cfg(not(debug_assertions))]
+const MAX_QUEUE_LEN: usize = 10;
+
+#[cfg(debug_assertions)]
+const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
+
+#[cfg(not(debug_assertions))]
+const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
+
+impl AmplitudeTelemetry {
+ pub fn new(client: Arc, cx: &AppContext) -> Arc {
+ let platform = cx.platform();
+ let this = Arc::new(Self {
+ http_client: client,
+ executor: cx.background().clone(),
+ session_id: SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_millis(),
+ state: Mutex::new(AmplitudeTelemetryState {
+ os_version: platform
+ .os_version()
+ .log_err()
+ .map(|v| v.to_string().into()),
+ os_name: platform.os_name().into(),
+ app_version: platform
+ .app_version()
+ .log_err()
+ .map(|v| v.to_string().into()),
+ device_id: None,
+ queue: Default::default(),
+ flush_task: Default::default(),
+ next_event_id: 0,
+ log_file: None,
+ metrics_id: None,
+ }),
+ });
+
+ if AMPLITUDE_API_KEY.is_some() {
+ this.executor
+ .spawn({
+ let this = this.clone();
+ async move {
+ if let Some(tempfile) = NamedTempFile::new().log_err() {
+ this.state.lock().log_file = Some(tempfile);
+ }
+ }
+ })
+ .detach();
+ }
+
+ this
+ }
+
+ pub fn log_file_path(&self) -> Option {
+ Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
+ }
+
+ pub fn start(self: &Arc, db: Db) {
+ let this = self.clone();
+ self.executor
+ .spawn(
+ async move {
+ let device_id = if let Ok(Some(device_id)) = db.read_kvp("device_id") {
+ device_id
+ } else {
+ let device_id = Uuid::new_v4().to_string();
+ db.write_kvp("device_id", &device_id)?;
+ device_id
+ };
+
+ let device_id = Some(Arc::from(device_id));
+ let mut state = this.state.lock();
+ state.device_id = device_id.clone();
+ for event in &mut state.queue {
+ event.device_id = device_id.clone();
+ }
+ if !state.queue.is_empty() {
+ drop(state);
+ this.flush();
+ }
+
+ anyhow::Ok(())
+ }
+ .log_err(),
+ )
+ .detach();
+ }
+
+ pub fn set_authenticated_user_info(
+ self: &Arc,
+ metrics_id: Option,
+ is_staff: bool,
+ ) {
+ let is_signed_in = metrics_id.is_some();
+ self.state.lock().metrics_id = metrics_id.map(|s| s.into());
+ if is_signed_in {
+ self.report_event_with_user_properties(
+ "$identify",
+ Default::default(),
+ json!({ "$set": { "staff": is_staff } }),
+ )
+ }
+ }
+
+ pub fn report_event(self: &Arc, kind: &str, properties: Value) {
+ self.report_event_with_user_properties(kind, properties, Default::default());
+ }
+
+ fn report_event_with_user_properties(
+ self: &Arc,
+ kind: &str,
+ properties: Value,
+ user_properties: Value,
+ ) {
+ if AMPLITUDE_API_KEY.is_none() {
+ return;
+ }
+
+ let mut state = self.state.lock();
+ let event = AmplitudeEvent {
+ event_type: kind.to_string(),
+ time: SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_millis(),
+ session_id: self.session_id,
+ event_properties: if let Value::Object(properties) = properties {
+ Some(properties)
+ } else {
+ None
+ },
+ user_properties: if let Value::Object(user_properties) = user_properties {
+ Some(user_properties)
+ } else {
+ None
+ },
+ user_id: state.metrics_id.clone(),
+ device_id: state.device_id.clone(),
+ os_name: state.os_name,
+ platform: "Zed",
+ os_version: state.os_version.clone(),
+ app_version: state.app_version.clone(),
+ event_id: post_inc(&mut state.next_event_id),
+ };
+ state.queue.push(event);
+ if state.device_id.is_some() {
+ if state.queue.len() >= MAX_QUEUE_LEN {
+ drop(state);
+ self.flush();
+ } else {
+ let this = self.clone();
+ let executor = self.executor.clone();
+ state.flush_task = Some(self.executor.spawn(async move {
+ executor.timer(DEBOUNCE_INTERVAL).await;
+ this.flush();
+ }));
+ }
+ }
+ }
+
+ fn flush(self: &Arc) {
+ let mut state = self.state.lock();
+ let events = mem::take(&mut state.queue);
+ state.flush_task.take();
+ drop(state);
+
+ if let Some(api_key) = AMPLITUDE_API_KEY.as_ref() {
+ let this = self.clone();
+ self.executor
+ .spawn(
+ async move {
+ let mut json_bytes = Vec::new();
+
+ if let Some(file) = &mut this.state.lock().log_file {
+ let file = file.as_file_mut();
+ for event in &events {
+ json_bytes.clear();
+ serde_json::to_writer(&mut json_bytes, event)?;
+ file.write_all(&json_bytes)?;
+ file.write(b"\n")?;
+ }
+ }
+
+ let batch = AmplitudeEventBatch { api_key, events };
+ json_bytes.clear();
+ serde_json::to_writer(&mut json_bytes, &batch)?;
+ let request =
+ Request::post(AMPLITUDE_EVENTS_URL).body(json_bytes.into())?;
+ this.http_client.send(request).await?;
+ Ok(())
+ }
+ .log_err(),
+ )
+ .detach();
+ }
+ }
+}
diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs
index 3878cc90c3826ebbb64b924be2b847d53858f878..8f6d4aa10d798686d1b59e3c46c3129beeffd280 100644
--- a/crates/client/src/client.rs
+++ b/crates/client/src/client.rs
@@ -1,11 +1,13 @@
#[cfg(any(test, feature = "test-support"))]
pub mod test;
+pub mod amplitude_telemetry;
pub mod channel;
pub mod http;
pub mod telemetry;
pub mod user;
+use amplitude_telemetry::AmplitudeTelemetry;
use anyhow::{anyhow, Context, Result};
use async_recursion::async_recursion;
use async_tungstenite::tungstenite::{
@@ -82,6 +84,7 @@ pub struct Client {
peer: Arc,
http: Arc,
telemetry: Arc,
+ amplitude_telemetry: Arc,
state: RwLock,
#[allow(clippy::type_complexity)]
@@ -261,6 +264,7 @@ impl Client {
id: 0,
peer: Peer::new(),
telemetry: Telemetry::new(http.clone(), cx),
+ amplitude_telemetry: AmplitudeTelemetry::new(http.clone(), cx),
http,
state: Default::default(),
@@ -373,6 +377,8 @@ impl Client {
}
Status::SignedOut | Status::UpgradeRequired => {
self.telemetry.set_authenticated_user_info(None, false);
+ self.amplitude_telemetry
+ .set_authenticated_user_info(None, false);
state._reconnect_task.take();
}
_ => {}
@@ -1013,6 +1019,7 @@ impl Client {
let platform = cx.platform();
let executor = cx.background();
let telemetry = self.telemetry.clone();
+ let amplitude_telemetry = self.amplitude_telemetry.clone();
let http = self.http.clone();
executor.clone().spawn(async move {
// Generate a pair of asymmetric encryption keys. The public key will be used by the
@@ -1097,6 +1104,7 @@ impl Client {
platform.activate(true);
telemetry.report_event("authenticate with browser", Default::default());
+ amplitude_telemetry.report_event("authenticate with browser", Default::default());
Ok(Credentials {
user_id: user_id.parse()?,
@@ -1208,14 +1216,17 @@ impl Client {
}
pub fn start_telemetry(&self, db: Db) {
- self.telemetry.start(db);
+ self.telemetry.start(db.clone());
+ self.amplitude_telemetry.start(db);
}
pub fn report_event(&self, kind: &str, properties: Value) {
- self.telemetry.report_event(kind, properties)
+ self.telemetry.report_event(kind, properties.clone());
+ self.amplitude_telemetry.report_event(kind, properties);
}
pub fn telemetry_log_file_path(&self) -> Option {
+ self.amplitude_telemetry.log_file_path();
self.telemetry.log_file_path()
}
}
diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs
index 6829eab53150901755c1f3d89025f3076acd34f0..02c1790664c28758cf366e43948b4aa2396032ec 100644
--- a/crates/client/src/telemetry.rs
+++ b/crates/client/src/telemetry.rs
@@ -24,7 +24,6 @@ use uuid::Uuid;
pub struct Telemetry {
http_client: Arc,
executor: Arc,
- session_id: u128,
state: Mutex,
}
@@ -35,43 +34,54 @@ struct TelemetryState {
app_version: Option>,
os_version: Option>,
os_name: &'static str,
- queue: Vec,
+ queue: Vec,
next_event_id: usize,
flush_task: Option>,
log_file: Option,
}
-const AMPLITUDE_EVENTS_URL: &'static str = "https://api2.amplitude.com/batch";
+const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track";
+const MIXPANEL_ENGAGE_URL: &'static str = "https://api.mixpanel.com/engage#profile-set";
lazy_static! {
- static ref AMPLITUDE_API_KEY: Option = std::env::var("ZED_AMPLITUDE_API_KEY")
+ static ref MIXPANEL_TOKEN: Option = std::env::var("ZED_MIXPANEL_TOKEN")
.ok()
- .or_else(|| option_env!("ZED_AMPLITUDE_API_KEY").map(|key| key.to_string()));
+ .or_else(|| option_env!("ZED_MIXPANEL_TOKEN").map(|key| key.to_string()));
}
-#[derive(Serialize)]
-struct AmplitudeEventBatch {
- api_key: &'static str,
- events: Vec,
+#[derive(Serialize, Debug)]
+struct MixpanelEvent {
+ event: String,
+ properties: MixpanelEventProperties,
}
-#[derive(Serialize)]
-struct AmplitudeEvent {
- #[serde(skip_serializing_if = "Option::is_none")]
- user_id: Option>,
- device_id: Option>,
- event_type: String,
- #[serde(skip_serializing_if = "Option::is_none")]
+#[derive(Serialize, Debug)]
+struct MixpanelEventProperties {
+ // Mixpanel required fields
+ #[serde(skip_serializing_if = "str::is_empty")]
+ token: &'static str,
+ time: u128,
+ distinct_id: Option>,
+ #[serde(rename = "$insert_id")]
+ insert_id: usize,
+ // Custom fields
+ #[serde(skip_serializing_if = "Option::is_none", flatten)]
event_properties: Option>,
- #[serde(skip_serializing_if = "Option::is_none")]
- user_properties: Option>,
os_name: &'static str,
os_version: Option>,
app_version: Option>,
+ signed_in: bool,
platform: &'static str,
- event_id: usize,
- session_id: u128,
- time: u128,
+}
+
+#[derive(Serialize)]
+struct MixpanelEngageRequest {
+ #[serde(rename = "$token")]
+ token: &'static str,
+ #[serde(rename = "$distinct_id")]
+ distinct_id: Arc,
+ #[serde(rename = "$set")]
+ set: Value,
}
#[cfg(debug_assertions)]
@@ -92,10 +102,6 @@ impl Telemetry {
let this = Arc::new(Self {
http_client: client,
executor: cx.background().clone(),
- session_id: SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap()
- .as_millis(),
state: Mutex::new(TelemetryState {
os_version: platform
.os_version()
@@ -107,15 +113,15 @@ impl Telemetry {
.log_err()
.map(|v| v.to_string().into()),
device_id: None,
+ metrics_id: None,
queue: Default::default(),
flush_task: Default::default(),
next_event_id: 0,
log_file: None,
- metrics_id: None,
}),
});
- if AMPLITUDE_API_KEY.is_some() {
+ if MIXPANEL_TOKEN.is_some() {
this.executor
.spawn({
let this = this.clone();
@@ -148,11 +154,14 @@ impl Telemetry {
device_id
};
- let device_id = Some(Arc::from(device_id));
+ let device_id: Arc = device_id.into();
let mut state = this.state.lock();
- state.device_id = device_id.clone();
+ state.device_id = Some(device_id.clone());
for event in &mut state.queue {
- event.device_id = device_id.clone();
+ event
+ .properties
+ .distinct_id
+ .get_or_insert_with(|| device_id.clone());
}
if !state.queue.is_empty() {
drop(state);
@@ -171,56 +180,57 @@ impl Telemetry {
metrics_id: Option,
is_staff: bool,
) {
- let is_signed_in = metrics_id.is_some();
- self.state.lock().metrics_id = metrics_id.map(|s| s.into());
- if is_signed_in {
- self.report_event_with_user_properties(
- "$identify",
- Default::default(),
- json!({ "$set": { "staff": is_staff } }),
- )
+ let this = self.clone();
+ let mut state = self.state.lock();
+ let device_id = state.device_id.clone();
+ let metrics_id: Option> = metrics_id.map(|id| id.into());
+ state.metrics_id = metrics_id.clone();
+ drop(state);
+
+ if let Some((token, device_id)) = MIXPANEL_TOKEN.as_ref().zip(device_id) {
+ self.executor
+ .spawn(
+ async move {
+ let json_bytes = serde_json::to_vec(&[MixpanelEngageRequest {
+ token,
+ distinct_id: device_id,
+ set: json!({ "staff": is_staff, "id": metrics_id }),
+ }])?;
+ let request = Request::post(MIXPANEL_ENGAGE_URL)
+ .header("Content-Type", "application/json")
+ .body(json_bytes.into())?;
+ this.http_client.send(request).await?;
+ Ok(())
+ }
+ .log_err(),
+ )
+ .detach();
}
}
pub fn report_event(self: &Arc, kind: &str, properties: Value) {
- self.report_event_with_user_properties(kind, properties, Default::default());
- }
-
- fn report_event_with_user_properties(
- self: &Arc,
- kind: &str,
- properties: Value,
- user_properties: Value,
- ) {
- if AMPLITUDE_API_KEY.is_none() {
- return;
- }
-
let mut state = self.state.lock();
- let event = AmplitudeEvent {
- event_type: kind.to_string(),
- time: SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap()
- .as_millis(),
- session_id: self.session_id,
- event_properties: if let Value::Object(properties) = properties {
- Some(properties)
- } else {
- None
- },
- user_properties: if let Value::Object(user_properties) = user_properties {
- Some(user_properties)
- } else {
- None
+ let event = MixpanelEvent {
+ event: kind.to_string(),
+ properties: MixpanelEventProperties {
+ token: "",
+ time: SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_millis(),
+ distinct_id: state.device_id.clone(),
+ insert_id: post_inc(&mut state.next_event_id),
+ event_properties: if let Value::Object(properties) = properties {
+ Some(properties)
+ } else {
+ None
+ },
+ os_name: state.os_name,
+ os_version: state.os_version.clone(),
+ app_version: state.app_version.clone(),
+ signed_in: state.metrics_id.is_some(),
+ platform: "Zed",
},
- user_id: state.metrics_id.clone(),
- device_id: state.device_id.clone(),
- os_name: state.os_name,
- platform: "Zed",
- os_version: state.os_version.clone(),
- app_version: state.app_version.clone(),
- event_id: post_inc(&mut state.next_event_id),
};
state.queue.push(event);
if state.device_id.is_some() {
@@ -240,11 +250,11 @@ impl Telemetry {
fn flush(self: &Arc) {
let mut state = self.state.lock();
- let events = mem::take(&mut state.queue);
+ let mut events = mem::take(&mut state.queue);
state.flush_task.take();
drop(state);
- if let Some(api_key) = AMPLITUDE_API_KEY.as_ref() {
+ if let Some(token) = MIXPANEL_TOKEN.as_ref() {
let this = self.clone();
self.executor
.spawn(
@@ -253,19 +263,21 @@ impl Telemetry {
if let Some(file) = &mut this.state.lock().log_file {
let file = file.as_file_mut();
- for event in &events {
+ for event in &mut events {
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, event)?;
file.write_all(&json_bytes)?;
file.write(b"\n")?;
+
+ event.properties.token = token;
}
}
- let batch = AmplitudeEventBatch { api_key, events };
json_bytes.clear();
- serde_json::to_writer(&mut json_bytes, &batch)?;
- let request =
- Request::post(AMPLITUDE_EVENTS_URL).body(json_bytes.into())?;
+ serde_json::to_writer(&mut json_bytes, &events)?;
+ let request = Request::post(MIXPANEL_EVENTS_URL)
+ .header("Content-Type", "application/json")
+ .body(json_bytes.into())?;
this.http_client.send(request).await?;
Ok(())
}
diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs
index 3c3d7e7fb3e199e16e28c106deda13d473a9c840..d06a6682c5e0fb2589d19fa23909e0988794b9bc 100644
--- a/crates/client/src/user.rs
+++ b/crates/client/src/user.rs
@@ -143,13 +143,24 @@ impl UserStore {
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
if let Some(info) = info {
client.telemetry.set_authenticated_user_info(
+ Some(info.metrics_id.clone()),
+ info.staff,
+ );
+ client.amplitude_telemetry.set_authenticated_user_info(
Some(info.metrics_id),
info.staff,
);
} else {
client.telemetry.set_authenticated_user_info(None, false);
+ client
+ .amplitude_telemetry
+ .set_authenticated_user_info(None, false);
}
+
client.telemetry.report_event("sign in", Default::default());
+ client
+ .amplitude_telemetry
+ .report_event("sign in", Default::default());
current_user_tx.send(user).await.ok();
}
}
diff --git a/crates/collab/.env.toml b/crates/collab/.env.toml
index 93a0a7f961a65edcf59eb13c4c85c4a566afa440..1945d9cb66b33ee36a8e17f4ebbc8af54f346c5c 100644
--- a/crates/collab/.env.toml
+++ b/crates/collab/.env.toml
@@ -2,6 +2,9 @@ DATABASE_URL = "postgres://postgres@localhost/zed"
HTTP_PORT = 8080
API_TOKEN = "secret"
INVITE_LINK_PREFIX = "http://localhost:3000/invites/"
+LIVE_KIT_SERVER = "http://localhost:7880"
+LIVE_KIT_KEY = "devkey"
+LIVE_KIT_SECRET = "secret"
# RUST_LOG=info
# LOG_JSON=true
diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml
index de41e8a1f3e0827e31aa7654381b07819d9a6abd..6145afad4873776a15767e751856eae084601b56 100644
--- a/crates/collab/Cargo.toml
+++ b/crates/collab/Cargo.toml
@@ -14,8 +14,10 @@ required-features = ["seed-support"]
[dependencies]
collections = { path = "../collections" }
+live_kit_server = { path = "../live_kit_server" }
rpc = { path = "../rpc" }
util = { path = "../util" }
+
anyhow = "1.0.40"
async-trait = "0.1.50"
async-tungstenite = "0.16"
@@ -60,15 +62,17 @@ editor = { path = "../editor", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
fs = { path = "../fs", features = ["test-support"] }
git = { path = "../git", features = ["test-support"] }
-log = { version = "0.4.16", features = ["kv_unstable_serde"] }
+live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
theme = { path = "../theme" }
workspace = { path = "../workspace", features = ["test-support"] }
+
ctor = "0.1"
env_logger = "0.9"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
util = { path = "../util" }
lazy_static = "1.4"
serde_json = { version = "1.0", features = ["preserve_order"] }
diff --git a/crates/collab/k8s/manifest.template.yml b/crates/collab/k8s/manifest.template.yml
index 94af6cc1521102a7294b87417f8b5bf8bffdedfa..a271ad8399c7a3a26bffbd28647aa4d7e73758db 100644
--- a/crates/collab/k8s/manifest.template.yml
+++ b/crates/collab/k8s/manifest.template.yml
@@ -72,6 +72,21 @@ spec:
secretKeyRef:
name: api
key: token
+ - name: LIVE_KIT_SERVER
+ valueFrom:
+ secretKeyRef:
+ name: livekit
+ key: server
+ - name: LIVE_KIT_KEY
+ valueFrom:
+ secretKeyRef:
+ name: livekit
+ key: key
+ - name: LIVE_KIT_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: livekit
+ key: secret
- name: INVITE_LINK_PREFIX
value: ${INVITE_LINK_PREFIX}
- name: RUST_LOG
diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs
index 4f97d93824520ec233c936c367910c25f11f637b..fbf45a379925edc8cf285f7d65e439886dd73b81 100644
--- a/crates/collab/src/api.rs
+++ b/crates/collab/src/api.rs
@@ -22,7 +22,7 @@ use time::OffsetDateTime;
use tower::ServiceBuilder;
use tracing::instrument;
-pub fn routes(rpc_server: &Arc, state: Arc) -> Router {
+pub fn routes(rpc_server: Arc, state: Arc) -> Router {
Router::new()
.route("/user", get(get_authenticated_user))
.route("/users", get(get_users).post(create_user))
@@ -50,7 +50,7 @@ pub fn routes(rpc_server: &Arc, state: Arc) -> Router Rc>> {
- let events = Rc::new(RefCell::new(Vec::new()));
- let active_call = cx.read(ActiveCall::global);
- cx.update({
- let events = events.clone();
- |cx| {
- cx.subscribe(&active_call, move |_, event, _| {
- events.borrow_mut().push(event.clone())
- })
- .detach()
- }
- });
- events
- }
+fn active_call_events(cx: &mut TestAppContext) -> Rc>> {
+ let events = Rc::new(RefCell::new(Vec::new()));
+ let active_call = cx.read(ActiveCall::global);
+ cx.update({
+ let events = events.clone();
+ |cx| {
+ cx.subscribe(&active_call, move |_, event, _| {
+ events.borrow_mut().push(event.clone())
+ })
+ .detach()
+ }
+ });
+ events
}
#[gpui::test(iterations = 10)]
@@ -984,15 +1074,9 @@ async fn test_room_location(
client_a.fs.insert_tree("/a", json!({})).await;
client_b.fs.insert_tree("/b", json!({})).await;
- let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
- let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
-
- server
- .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
- .await;
-
let active_call_a = cx_a.read(ActiveCall::global);
- let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
+ let active_call_b = cx_b.read(ActiveCall::global);
+
let a_notified = Rc::new(Cell::new(false));
cx_a.update({
let notified = a_notified.clone();
@@ -1002,8 +1086,6 @@ async fn test_room_location(
}
});
- let active_call_b = cx_b.read(ActiveCall::global);
- let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
let b_notified = Rc::new(Cell::new(false));
cx_b.update({
let b_notified = b_notified.clone();
@@ -1013,10 +1095,18 @@ async fn test_room_location(
}
});
- room_a
- .update(cx_a, |room, cx| room.set_location(Some(&project_a), cx))
+ let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
.await
.unwrap();
+ let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
+
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
+ let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
deterministic.run_until_parked();
assert!(a_notified.take());
assert_eq!(
@@ -1071,8 +1161,8 @@ async fn test_room_location(
)]
);
- room_b
- .update(cx_b, |room, cx| room.set_location(Some(&project_b), cx))
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
.unwrap();
deterministic.run_until_parked();
@@ -1097,8 +1187,8 @@ async fn test_room_location(
)]
);
- room_b
- .update(cx_b, |room, cx| room.set_location(None, cx))
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(None, cx))
.await
.unwrap();
deterministic.run_until_parked();
@@ -4968,7 +5058,11 @@ async fn test_contact_requests(
}
#[gpui::test(iterations = 10)]
-async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+async fn test_following(
+ deterministic: Arc,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
cx_a.foreground().forbid_parking();
cx_a.update(editor::init);
cx_b.update(editor::init);
@@ -4980,6 +5074,7 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
client_a
.fs
@@ -4993,11 +5088,20 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
+
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
// Client A opens some editors.
let workspace_a = client_a.build_workspace(&project_a, cx_a);
@@ -5139,7 +5243,7 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
workspace_a.update(cx_a, |workspace, cx| {
workspace.activate_item(&editor_a2, cx)
});
- cx_a.foreground().run_until_parked();
+ deterministic.run_until_parked();
assert_eq!(
workspace_b.read_with(cx_b, |workspace, cx| workspace
.active_item(cx)
@@ -5169,9 +5273,62 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
editor_a1.id()
);
+ // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
+ let display = MacOSDisplay::new();
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(None, cx))
+ .await
+ .unwrap();
+ active_call_b
+ .update(cx_b, |call, cx| {
+ call.room().unwrap().update(cx, |room, cx| {
+ room.set_display_sources(vec![display.clone()]);
+ room.share_screen(cx)
+ })
+ })
+ .await
+ .unwrap();
+ deterministic.run_until_parked();
+ let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
+ workspace
+ .active_item(cx)
+ .unwrap()
+ .downcast::()
+ .unwrap()
+ });
+
+ // Client B activates Zed again, which causes the previous editor to become focused again.
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
+ deterministic.run_until_parked();
+ assert_eq!(
+ workspace_a.read_with(cx_a, |workspace, cx| workspace
+ .active_item(cx)
+ .unwrap()
+ .id()),
+ editor_a1.id()
+ );
+
+ // Client B activates an external window again, and the previously-opened screen-sharing item
+ // gets activated.
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(None, cx))
+ .await
+ .unwrap();
+ deterministic.run_until_parked();
+ assert_eq!(
+ workspace_a.read_with(cx_a, |workspace, cx| workspace
+ .active_item(cx)
+ .unwrap()
+ .id()),
+ shared_screen.id()
+ );
+
// Following interrupts when client B disconnects.
client_b.disconnect(&cx_b.to_async()).unwrap();
- cx_a.foreground().run_until_parked();
+ deterministic.run_until_parked();
assert_eq!(
workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
None
@@ -5191,6 +5348,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
// Client A shares a project.
client_a
@@ -5206,6 +5364,10 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -5213,6 +5375,10 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
// Client B joins the project.
let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
// Client A opens some editors.
let workspace_a = client_a.build_workspace(&project_a, cx_a);
@@ -5360,6 +5526,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
// Client A shares a project.
client_a
@@ -5374,11 +5541,20 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
+
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
// Client A opens some editors.
let workspace_a = client_a.build_workspace(&project_a, cx_a);
@@ -6138,6 +6314,7 @@ struct TestServer {
connection_killers: Arc>>>,
forbid_connections: Arc,
_test_db: TestDb,
+ test_live_kit_server: Arc,
}
impl TestServer {
@@ -6145,8 +6322,18 @@ impl TestServer {
foreground: Rc,
background: Arc,
) -> Self {
+ static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0);
+
let test_db = TestDb::fake(background.clone());
- let app_state = Self::build_app_state(&test_db).await;
+ let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst);
+ 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),
+ background.clone(),
+ )
+ .unwrap();
+ let app_state = Self::build_app_state(&test_db, &live_kit_server).await;
let peer = Peer::new();
let notifications = mpsc::unbounded();
let server = Server::new(app_state.clone(), Some(notifications.0));
@@ -6159,6 +6346,7 @@ impl TestServer {
connection_killers: Default::default(),
forbid_connections: Default::default(),
_test_db: test_db,
+ test_live_kit_server: live_kit_server,
}
}
@@ -6354,9 +6542,13 @@ impl TestServer {
}
}
- async fn build_app_state(test_db: &TestDb) -> Arc {
+ async fn build_app_state(
+ test_db: &TestDb,
+ fake_server: &live_kit_client::TestServer,
+ ) -> Arc {
Arc::new(AppState {
db: test_db.db().clone(),
+ live_kit_client: Some(Arc::new(fake_server.create_api_client())),
config: Default::default(),
})
}
@@ -6388,6 +6580,7 @@ impl Deref for TestServer {
impl Drop for TestServer {
fn drop(&mut self) {
self.peer.reset();
+ self.test_live_kit_server.teardown().unwrap();
}
}
diff --git a/crates/collab/src/main.rs b/crates/collab/src/main.rs
index 0308b21e2b294dec8cd740c7eee20cfae1e6b14d..8085fd8026f443ec80d15792fe62062f6193bae1 100644
--- a/crates/collab/src/main.rs
+++ b/crates/collab/src/main.rs
@@ -9,6 +9,7 @@ mod db_tests;
#[cfg(test)]
mod integration_tests;
+use crate::rpc::ResultExt as _;
use anyhow::anyhow;
use axum::{routing::get, Router};
use collab::{Error, Result};
@@ -21,6 +22,7 @@ use std::{
sync::Arc,
time::Duration,
};
+use tokio::signal;
use tracing_log::LogTracer;
use tracing_subscriber::{filter::EnvFilter, fmt::format::JsonFields, Layer};
use util::ResultExt;
@@ -33,6 +35,9 @@ pub struct Config {
pub database_url: String,
pub api_token: String,
pub invite_link_prefix: String,
+ pub live_kit_server: Option,
+ pub live_kit_key: Option,
+ pub live_kit_secret: Option,
pub rust_log: Option,
pub log_json: Option,
}
@@ -45,9 +50,37 @@ pub struct MigrateConfig {
pub struct AppState {
db: Arc,
+ live_kit_client: Option>,
config: Config,
}
+impl AppState {
+ async fn new(config: Config) -> Result> {
+ let db = PostgresDb::new(&config.database_url, 5).await?;
+ let live_kit_client = if let Some(((server, key), secret)) = config
+ .live_kit_server
+ .as_ref()
+ .zip(config.live_kit_key.as_ref())
+ .zip(config.live_kit_secret.as_ref())
+ {
+ Some(Arc::new(live_kit_server::api::LiveKitClient::new(
+ server.clone(),
+ key.clone(),
+ secret.clone(),
+ )) as Arc)
+ } else {
+ None
+ };
+
+ let this = Self {
+ db: Arc::new(db),
+ live_kit_client,
+ config,
+ };
+ Ok(Arc::new(this))
+ }
+}
+
#[tokio::main]
async fn main() -> Result<()> {
if let Err(error) = env::load_dotenv() {
@@ -83,14 +116,9 @@ async fn main() -> Result<()> {
}
Some("serve") => {
let config = envy::from_env::().expect("error loading config");
- let db = PostgresDb::new(&config.database_url, 5).await?;
-
init_tracing(&config);
- let state = Arc::new(AppState {
- db: Arc::new(db),
- config,
- });
+ let state = AppState::new(config).await?;
let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port))
.expect("failed to bind TCP listener");
@@ -98,12 +126,13 @@ async fn main() -> Result<()> {
rpc_server
.start_recording_project_activity(Duration::from_secs(5 * 60), rpc::RealExecutor);
- let app = api::routes(&rpc_server, state.clone())
- .merge(rpc::routes(rpc_server))
+ let app = api::routes(rpc_server.clone(), state.clone())
+ .merge(rpc::routes(rpc_server.clone()))
.merge(Router::new().route("/", get(handle_root)));
axum::Server::from_tcp(listener)?
.serve(app.into_make_service_with_connect_info::())
+ .with_graceful_shutdown(graceful_shutdown(rpc_server, state))
.await?;
}
_ => {
@@ -148,3 +177,52 @@ pub fn init_tracing(config: &Config) -> Option<()> {
None
}
+
+async fn graceful_shutdown(rpc_server: Arc, state: Arc) {
+ let ctrl_c = async {
+ signal::ctrl_c()
+ .await
+ .expect("failed to install Ctrl+C handler");
+ };
+
+ #[cfg(unix)]
+ let terminate = async {
+ signal::unix::signal(signal::unix::SignalKind::terminate())
+ .expect("failed to install signal handler")
+ .recv()
+ .await;
+ };
+
+ #[cfg(not(unix))]
+ let terminate = std::future::pending::<()>();
+
+ tokio::select! {
+ _ = ctrl_c => {},
+ _ = terminate => {},
+ }
+
+ if let Some(live_kit) = state.live_kit_client.as_ref() {
+ let deletions = rpc_server
+ .store()
+ .await
+ .rooms()
+ .values()
+ .map(|room| {
+ let name = room.live_kit_room.clone();
+ async {
+ live_kit.delete_room(name).await.trace_err();
+ }
+ })
+ .collect::>();
+
+ tracing::info!("deleting all live-kit rooms");
+ if let Err(_) = tokio::time::timeout(
+ Duration::from_secs(10),
+ futures::future::join_all(deletions),
+ )
+ .await
+ {
+ tracing::error!("timed out waiting for live-kit room deletion");
+ }
+ }
+}
diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs
index f2ffb1dc5eeec21b1e05064f22daf19366d72bed..059a1d46e673b56d050e97d926ec366fab024c08 100644
--- a/crates/collab/src/rpc.rs
+++ b/crates/collab/src/rpc.rs
@@ -50,6 +50,7 @@ use std::{
},
time::Duration,
};
+pub use store::{Store, Worktree};
use time::OffsetDateTime;
use tokio::{
sync::{Mutex, MutexGuard},
@@ -58,8 +59,6 @@ use tokio::{
use tower::ServiceBuilder;
use tracing::{info_span, instrument, Instrument};
-pub use store::{Store, Worktree};
-
lazy_static! {
static ref METRIC_CONNECTIONS: IntGauge =
register_int_gauge!("connections", "number of connections").unwrap();
@@ -477,6 +476,7 @@ impl Server {
let mut projects_to_unshare = Vec::new();
let mut contacts_to_update = HashSet::default();
+ let mut room_left = None;
{
let mut store = self.store().await;
@@ -509,23 +509,24 @@ impl Server {
});
}
+ if let Some(room) = removed_connection.room {
+ self.room_updated(&room);
+ room_left = Some(self.room_left(&room, connection_id));
+ }
+
+ contacts_to_update.insert(removed_connection.user_id);
for connection_id in removed_connection.canceled_call_connection_ids {
self.peer
.send(connection_id, proto::CallCanceled {})
.trace_err();
contacts_to_update.extend(store.user_id_for_connection(connection_id).ok());
}
-
- if let Some(room) = removed_connection
- .room_id
- .and_then(|room_id| store.room(room_id))
- {
- self.room_updated(room);
- }
-
- contacts_to_update.insert(removed_connection.user_id);
};
+ if let Some(room_left) = room_left {
+ room_left.await.trace_err();
+ }
+
for user_id in contacts_to_update {
self.update_user_contacts(user_id).await.trace_err();
}
@@ -607,13 +608,42 @@ impl Server {
response: Response,
) -> Result<()> {
let user_id;
- let room_id;
+ let room;
{
let mut store = self.store().await;
user_id = store.user_id_for_connection(request.sender_id)?;
- room_id = store.create_room(request.sender_id)?;
+ room = store.create_room(request.sender_id)?.clone();
}
- response.send(proto::CreateRoomResponse { id: room_id })?;
+
+ let live_kit_connection_info =
+ if let Some(live_kit) = self.app_state.live_kit_client.as_ref() {
+ if let Some(_) = live_kit
+ .create_room(room.live_kit_room.clone())
+ .await
+ .trace_err()
+ {
+ if let Some(token) = live_kit
+ .room_token(&room.live_kit_room, &request.sender_id.to_string())
+ .trace_err()
+ {
+ Some(proto::LiveKitConnectionInfo {
+ server_url: live_kit.url().into(),
+ token,
+ })
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ } else {
+ None
+ };
+
+ response.send(proto::CreateRoomResponse {
+ room: Some(room),
+ live_kit_connection_info,
+ })?;
self.update_user_contacts(user_id).await?;
Ok(())
}
@@ -634,8 +664,27 @@ impl Server {
.send(recipient_id, proto::CallCanceled {})
.trace_err();
}
+
+ let live_kit_connection_info =
+ if let Some(live_kit) = self.app_state.live_kit_client.as_ref() {
+ if let Some(token) = live_kit
+ .room_token(&room.live_kit_room, &request.sender_id.to_string())
+ .trace_err()
+ {
+ Some(proto::LiveKitConnectionInfo {
+ server_url: live_kit.url().into(),
+ token,
+ })
+ } else {
+ None
+ }
+ } else {
+ None
+ };
+
response.send(proto::JoinRoomResponse {
room: Some(room.clone()),
+ live_kit_connection_info,
})?;
self.room_updated(room);
}
@@ -645,6 +694,7 @@ impl Server {
async fn leave_room(self: Arc, message: TypedEnvelope) -> Result<()> {
let mut contacts_to_update = HashSet::default();
+ let room_left;
{
let mut store = self.store().await;
let user_id = store.user_id_for_connection(message.sender_id)?;
@@ -683,9 +733,8 @@ impl Server {
}
}
- if let Some(room) = left_room.room {
- self.room_updated(room);
- }
+ self.room_updated(&left_room.room);
+ room_left = self.room_left(&left_room.room, message.sender_id);
for connection_id in left_room.canceled_call_connection_ids {
self.peer
@@ -695,6 +744,7 @@ impl Server {
}
}
+ room_left.await.trace_err();
for user_id in contacts_to_update {
self.update_user_contacts(user_id).await?;
}
@@ -843,6 +893,29 @@ impl Server {
}
}
+ fn room_left(
+ &self,
+ room: &proto::Room,
+ connection_id: ConnectionId,
+ ) -> impl Future> {
+ let client = self.app_state.live_kit_client.clone();
+ let room_name = room.live_kit_room.clone();
+ let participant_count = room.participants.len();
+ async move {
+ if let Some(client) = client {
+ client
+ .remove_participant(room_name.clone(), connection_id.to_string())
+ .await?;
+
+ if participant_count == 0 {
+ client.delete_room(room_name).await?;
+ }
+ }
+
+ Ok(())
+ }
+ }
+
async fn share_project(
self: Arc,
request: TypedEnvelope,
diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs
index f48773518825d911ab999a4722e33ff935d04c75..a7abce7094b1c8fdfb062d98e21c9f859b58cdfa 100644
--- a/crates/collab/src/rpc/store.rs
+++ b/crates/collab/src/rpc/store.rs
@@ -1,9 +1,10 @@
use crate::db::{self, ChannelId, ProjectId, UserId};
use anyhow::{anyhow, Result};
use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet};
+use nanoid::nanoid;
use rpc::{proto, ConnectionId};
use serde::Serialize;
-use std::{mem, path::PathBuf, str, time::Duration};
+use std::{borrow::Cow, mem, path::PathBuf, str, time::Duration};
use time::OffsetDateTime;
use tracing::instrument;
use util::post_inc;
@@ -85,12 +86,12 @@ pub struct Channel {
pub type ReplicaId = u16;
#[derive(Default)]
-pub struct RemovedConnectionState {
+pub struct RemovedConnectionState<'a> {
pub user_id: UserId,
pub hosted_projects: Vec,
pub guest_projects: Vec,
pub contact_ids: HashSet,
- pub room_id: Option,
+ pub room: Option>,
pub canceled_call_connection_ids: Vec,
}
@@ -103,7 +104,7 @@ pub struct LeftProject {
}
pub struct LeftRoom<'a> {
- pub room: Option<&'a proto::Room>,
+ pub room: Cow<'a, proto::Room>,
pub unshared_projects: Vec,
pub left_projects: Vec,
pub canceled_call_connection_ids: Vec,
@@ -219,11 +220,11 @@ impl Store {
let left_room = self.leave_room(room_id, connection_id)?;
result.hosted_projects = left_room.unshared_projects;
result.guest_projects = left_room.left_projects;
- result.room_id = Some(room_id);
+ result.room = Some(Cow::Owned(left_room.room.into_owned()));
result.canceled_call_connection_ids = left_room.canceled_call_connection_ids;
} else if connected_user.connection_ids.len() == 1 {
- self.decline_call(room_id, connection_id)?;
- result.room_id = Some(room_id);
+ let (room, _) = self.decline_call(room_id, connection_id)?;
+ result.room = Some(Cow::Owned(room.clone()));
}
}
@@ -345,7 +346,7 @@ impl Store {
}
}
- pub fn create_room(&mut self, creator_connection_id: ConnectionId) -> Result {
+ pub fn create_room(&mut self, creator_connection_id: ConnectionId) -> Result<&proto::Room> {
let connection = self
.connections
.get_mut(&creator_connection_id)
@@ -359,19 +360,23 @@ impl Store {
"can't create a room with an active call"
);
- let mut room = proto::Room::default();
- room.participants.push(proto::Participant {
- user_id: connection.user_id.to_proto(),
- peer_id: creator_connection_id.0,
- projects: Default::default(),
- location: Some(proto::ParticipantLocation {
- variant: Some(proto::participant_location::Variant::External(
- proto::participant_location::External {},
- )),
- }),
- });
-
let room_id = post_inc(&mut self.next_room_id);
+ let room = proto::Room {
+ id: room_id,
+ participants: vec![proto::Participant {
+ user_id: connection.user_id.to_proto(),
+ peer_id: creator_connection_id.0,
+ projects: Default::default(),
+ location: Some(proto::ParticipantLocation {
+ variant: Some(proto::participant_location::Variant::External(
+ proto::participant_location::External {},
+ )),
+ }),
+ }],
+ pending_participant_user_ids: Default::default(),
+ live_kit_room: nanoid!(30),
+ };
+
self.rooms.insert(room_id, room);
connected_user.active_call = Some(Call {
caller_user_id: connection.user_id,
@@ -379,7 +384,7 @@ impl Store {
connection_id: Some(creator_connection_id),
initial_project_id: None,
});
- Ok(room_id)
+ Ok(self.rooms.get(&room_id).unwrap())
}
pub fn join_room(
@@ -496,12 +501,14 @@ impl Store {
}
});
- if room.participants.is_empty() && room.pending_participant_user_ids.is_empty() {
- self.rooms.remove(&room_id);
- }
+ let room = if room.participants.is_empty() {
+ Cow::Owned(self.rooms.remove(&room_id).unwrap())
+ } else {
+ Cow::Borrowed(self.rooms.get(&room_id).unwrap())
+ };
Ok(LeftRoom {
- room: self.rooms.get(&room_id),
+ room,
unshared_projects,
left_projects,
canceled_call_connection_ids,
@@ -512,6 +519,10 @@ impl Store {
self.rooms.get(&room_id)
}
+ pub fn rooms(&self) -> &BTreeMap {
+ &self.rooms
+ }
+
pub fn call(
&mut self,
room_id: RoomId,
diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs
index 1279d3043748e7b8186364f926884faac0a3785b..2a8870fe66dd7d64d509e1fa8105c88c76ce5612 100644
--- a/crates/collab_ui/src/collab_titlebar_item.rs
+++ b/crates/collab_ui/src/collab_titlebar_item.rs
@@ -10,17 +10,21 @@ use gpui::{
geometry::{rect::RectF, vector::vec2f, PathBuilder},
json::{self, ToJson},
Border, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
- Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
+ Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use settings::Settings;
use std::ops::Range;
use theme::Theme;
use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
-actions!(collab, [ToggleCollaborationMenu, ShareProject]);
+actions!(
+ collab,
+ [ToggleCollaborationMenu, ToggleScreenSharing, ShareProject]
+);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
+ cx.add_action(CollabTitlebarItem::toggle_screen_sharing);
cx.add_action(CollabTitlebarItem::share_project);
}
@@ -48,10 +52,12 @@ impl View for CollabTitlebarItem {
};
let theme = cx.global::().theme.clone();
- let project = workspace.read(cx).project().read(cx);
let mut container = Flex::row();
+ container.add_children(self.render_toggle_screen_sharing_button(&theme, cx));
+
if workspace.read(cx).client().status().borrow().is_connected() {
+ let project = workspace.read(cx).project().read(cx);
if project.is_shared()
|| project.is_remote()
|| ActiveCall::global(cx).read(cx).room().is_none()
@@ -114,19 +120,15 @@ impl CollabTitlebarItem {
}
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext) {
- let workspace = self.workspace.upgrade(cx);
- let room = ActiveCall::global(cx).read(cx).room().cloned();
- if let Some((workspace, room)) = workspace.zip(room) {
- let workspace = workspace.read(cx);
+ if let Some(workspace) = self.workspace.upgrade(cx) {
let project = if active {
- Some(workspace.project().clone())
+ Some(workspace.read(cx).project().clone())
} else {
None
};
- room.update(cx, |room, cx| {
- room.set_location(project.as_ref(), cx)
- .detach_and_log_err(cx);
- });
+ ActiveCall::global(cx)
+ .update(cx, |call, cx| call.set_location(project.as_ref(), cx))
+ .detach_and_log_err(cx);
}
}
@@ -169,6 +171,19 @@ impl CollabTitlebarItem {
cx.notify();
}
+ pub fn toggle_screen_sharing(&mut self, _: &ToggleScreenSharing, cx: &mut ViewContext) {
+ if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
+ let toggle_screen_sharing = room.update(cx, |room, cx| {
+ if room.is_screen_sharing() {
+ Task::ready(room.unshare_screen(cx))
+ } else {
+ room.share_screen(cx)
+ }
+ });
+ toggle_screen_sharing.detach_and_log_err(cx);
+ }
+ }
+
fn render_toggle_contacts_button(
&self,
theme: &Theme,
@@ -237,6 +252,56 @@ impl CollabTitlebarItem {
.boxed()
}
+ fn render_toggle_screen_sharing_button(
+ &self,
+ theme: &Theme,
+ cx: &mut RenderContext,
+ ) -> Option {
+ let active_call = ActiveCall::global(cx);
+ let room = active_call.read(cx).room().cloned()?;
+ let icon;
+ let tooltip;
+
+ if room.read(cx).is_screen_sharing() {
+ icon = "icons/disable_screen_sharing_12.svg";
+ tooltip = "Stop Sharing Screen"
+ } else {
+ icon = "icons/enable_screen_sharing_12.svg";
+ tooltip = "Share Screen";
+ }
+
+ let titlebar = &theme.workspace.titlebar;
+ Some(
+ MouseEventHandler::::new(0, cx, |state, _| {
+ let style = titlebar.call_control.style_for(state, false);
+ Svg::new(icon)
+ .with_color(style.color)
+ .constrained()
+ .with_width(style.icon_width)
+ .aligned()
+ .constrained()
+ .with_width(style.button_width)
+ .with_height(style.button_width)
+ .contained()
+ .with_style(style.container)
+ .boxed()
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |_, cx| {
+ cx.dispatch_action(ToggleScreenSharing);
+ })
+ .with_tooltip::(
+ 0,
+ tooltip.into(),
+ Some(Box::new(ToggleScreenSharing)),
+ theme.tooltip.clone(),
+ cx,
+ )
+ .aligned()
+ .boxed(),
+ )
+ }
+
fn render_share_button(&self, theme: &Theme, cx: &mut RenderContext) -> ElementBox {
enum Share {}
diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs
index 7a51cc83ec5877e9b5485edfcbb5fc78515d6476..1947d14a1627009516c349751db2ff51e485f5d0 100644
--- a/crates/collab_ui/src/contact_list.rs
+++ b/crates/collab_ui/src/contact_list.rs
@@ -17,7 +17,7 @@ use serde::Deserialize;
use settings::Settings;
use theme::IconButton;
use util::ResultExt;
-use workspace::JoinProject;
+use workspace::{JoinProject, OpenSharedScreen};
impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]);
impl_internal_actions!(contact_list, [ToggleExpanded, Call, LeaveCall]);
@@ -67,6 +67,10 @@ enum ContactEntry {
host_user_id: u64,
is_last: bool,
},
+ ParticipantScreen {
+ peer_id: PeerId,
+ is_last: bool,
+ },
IncomingRequest(Arc),
OutgoingRequest(Arc),
Contact(Arc),
@@ -97,6 +101,16 @@ impl PartialEq for ContactEntry {
return project_id_1 == project_id_2;
}
}
+ ContactEntry::ParticipantScreen {
+ peer_id: peer_id_1, ..
+ } => {
+ if let ContactEntry::ParticipantScreen {
+ peer_id: peer_id_2, ..
+ } = other
+ {
+ return peer_id_1 == peer_id_2;
+ }
+ }
ContactEntry::IncomingRequest(user_1) => {
if let ContactEntry::IncomingRequest(user_2) = other {
return user_1.id == user_2.id;
@@ -216,6 +230,15 @@ impl ContactList {
&theme.contact_list,
cx,
),
+ ContactEntry::ParticipantScreen { peer_id, is_last } => {
+ Self::render_participant_screen(
+ *peer_id,
+ *is_last,
+ is_selected,
+ &theme.contact_list,
+ cx,
+ )
+ }
ContactEntry::IncomingRequest(user) => Self::render_contact_request(
user.clone(),
this.user_store.clone(),
@@ -347,6 +370,9 @@ impl ContactList {
follow_user_id: *host_user_id,
});
}
+ ContactEntry::ParticipantScreen { peer_id, .. } => {
+ cx.dispatch_action(OpenSharedScreen { peer_id: *peer_id });
+ }
_ => {}
}
}
@@ -430,11 +456,10 @@ impl ContactList {
executor.clone(),
));
for mat in matches {
- let participant = &room.remote_participants()[&PeerId(mat.candidate_id as u32)];
+ let peer_id = PeerId(mat.candidate_id as u32);
+ let participant = &room.remote_participants()[&peer_id];
participant_entries.push(ContactEntry::CallParticipant {
- user: room.remote_participants()[&PeerId(mat.candidate_id as u32)]
- .user
- .clone(),
+ user: participant.user.clone(),
is_pending: false,
});
let mut projects = participant.projects.iter().peekable();
@@ -443,7 +468,13 @@ impl ContactList {
project_id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
host_user_id: participant.user.id,
- is_last: projects.peek().is_none(),
+ is_last: projects.peek().is_none() && participant.tracks.is_empty(),
+ });
+ }
+ if !participant.tracks.is_empty() {
+ participant_entries.push(ContactEntry::ParticipantScreen {
+ peer_id,
+ is_last: true,
});
}
}
@@ -763,6 +794,102 @@ impl ContactList {
.boxed()
}
+ fn render_participant_screen(
+ peer_id: PeerId,
+ is_last: bool,
+ is_selected: bool,
+ theme: &theme::ContactList,
+ cx: &mut RenderContext,
+ ) -> ElementBox {
+ let font_cache = cx.font_cache();
+ let host_avatar_height = theme
+ .contact_avatar
+ .width
+ .or(theme.contact_avatar.height)
+ .unwrap_or(0.);
+ let row = &theme.project_row.default;
+ let tree_branch = theme.tree_branch;
+ let line_height = row.name.text.line_height(font_cache);
+ let cap_height = row.name.text.cap_height(font_cache);
+ let baseline_offset =
+ row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
+
+ MouseEventHandler::::new(peer_id.0 as usize, cx, |mouse_state, _| {
+ let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
+ let row = theme.project_row.style_for(mouse_state, is_selected);
+
+ Flex::row()
+ .with_child(
+ Stack::new()
+ .with_child(
+ Canvas::new(move |bounds, _, cx| {
+ let start_x = bounds.min_x() + (bounds.width() / 2.)
+ - (tree_branch.width / 2.);
+ let end_x = bounds.max_x();
+ let start_y = bounds.min_y();
+ let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
+
+ cx.scene.push_quad(gpui::Quad {
+ bounds: RectF::from_points(
+ vec2f(start_x, start_y),
+ vec2f(
+ start_x + tree_branch.width,
+ if is_last { end_y } else { bounds.max_y() },
+ ),
+ ),
+ background: Some(tree_branch.color),
+ border: gpui::Border::default(),
+ corner_radius: 0.,
+ });
+ cx.scene.push_quad(gpui::Quad {
+ bounds: RectF::from_points(
+ vec2f(start_x, end_y),
+ vec2f(end_x, end_y + tree_branch.width),
+ ),
+ background: Some(tree_branch.color),
+ border: gpui::Border::default(),
+ corner_radius: 0.,
+ });
+ })
+ .boxed(),
+ )
+ .constrained()
+ .with_width(host_avatar_height)
+ .boxed(),
+ )
+ .with_child(
+ Svg::new("icons/disable_screen_sharing_12.svg")
+ .with_color(row.icon.color)
+ .constrained()
+ .with_width(row.icon.width)
+ .aligned()
+ .left()
+ .contained()
+ .with_style(row.icon.container)
+ .boxed(),
+ )
+ .with_child(
+ Label::new("Screen".into(), row.name.text.clone())
+ .aligned()
+ .left()
+ .contained()
+ .with_style(row.name.container)
+ .flex(1., false)
+ .boxed(),
+ )
+ .constrained()
+ .with_height(theme.row_height)
+ .contained()
+ .with_style(row.container)
+ .boxed()
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |_, cx| {
+ cx.dispatch_action(OpenSharedScreen { peer_id });
+ })
+ .boxed()
+ }
+
fn render_header(
section: Section,
theme: &theme::ContactList,
@@ -1035,25 +1162,11 @@ impl ContactList {
fn call(&mut self, action: &Call, cx: &mut ViewContext) {
let recipient_user_id = action.recipient_user_id;
let initial_project = action.initial_project.clone();
- let window_id = cx.window_id();
-
- let active_call = ActiveCall::global(cx);
- cx.spawn_weak(|_, mut cx| async move {
- active_call
- .update(&mut cx, |active_call, cx| {
- active_call.invite(recipient_user_id, initial_project.clone(), cx)
- })
- .await?;
- if cx.update(|cx| cx.window_is_active(window_id)) {
- active_call
- .update(&mut cx, |call, cx| {
- call.set_location(initial_project.as_ref(), cx)
- })
- .await?;
- }
- anyhow::Ok(())
- })
- .detach_and_log_err(cx);
+ ActiveCall::global(cx)
+ .update(cx, |call, cx| {
+ call.invite(recipient_user_id, initial_project.clone(), cx)
+ })
+ .detach_and_log_err(cx);
}
fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext) {
diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs
index a17e11b079f42c2bb863b0dee8cc8f734fcc0aca..e5ded819afeb0062ea2ce76b07fdf419cea45bb8 100644
--- a/crates/collab_ui/src/project_shared_notification.rs
+++ b/crates/collab_ui/src/project_shared_notification.rs
@@ -62,6 +62,7 @@ pub fn init(cx: &mut MutableAppContext) {
cx.remove_window(window_id);
}
}
+ _ => {}
})
.detach();
}
diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs
index 836d586c26bea742d7b31832d0b17b64d0295a05..3fb0119782cdd10aa8698362b8c6e9ce3812ff29 100644
--- a/crates/gpui/build.rs
+++ b/crates/gpui/build.rs
@@ -57,7 +57,7 @@ fn compile_metal_shaders() {
"macosx",
"metal",
"-gline-tables-only",
- "-mmacosx-version-min=10.14",
+ "-mmacosx-version-min=10.15.7",
"-MO",
"-c",
shader_path,
diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs
index a9020cf35088a30488a8fb62ed1d0a547d37227b..5cbc786b725adce8ab4dc9997990b58c4e1a5197 100644
--- a/crates/gpui/src/app.rs
+++ b/crates/gpui/src/app.rs
@@ -3835,6 +3835,11 @@ impl<'a, T: View> ViewContext<'a, T> {
self.app.notify_view(self.window_id, self.view_id);
}
+ pub fn dispatch_action(&mut self, action: impl Action) {
+ self.app
+ .dispatch_action_at(self.window_id, self.view_id, action)
+ }
+
pub fn dispatch_any_action(&mut self, action: Box) {
self.app
.dispatch_any_action_at(self.window_id, self.view_id, action)
diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs
index a648df6095820054ef52ce0a600af13a39ddeac7..9f11f09f8e9e33ebcd414e15546419ad00317ffe 100644
--- a/crates/gpui/src/elements.rs
+++ b/crates/gpui/src/elements.rs
@@ -464,7 +464,7 @@ pub trait ParentElement<'a>: Extend + Sized {
impl<'a, T> ParentElement<'a> for T where T: Extend {}
-fn constrain_size_preserving_aspect_ratio(max_size: Vector2F, size: Vector2F) -> Vector2F {
+pub fn constrain_size_preserving_aspect_ratio(max_size: Vector2F, size: Vector2F) -> Vector2F {
if max_size.x().is_infinite() && max_size.y().is_infinite() {
size
} else if max_size.x().is_infinite() || max_size.x() / max_size.y() > size.x() / size.y() {
diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml
index 96feadbfbc0b032942c478d82380f20c8c0535a0..9a11b8051142a3fa76e04a9ab39d2522bf9484f9 100644
--- a/crates/language/Cargo.toml
+++ b/crates/language/Cargo.toml
@@ -36,7 +36,7 @@ text = { path = "../text" }
theme = { path = "../theme" }
util = { path = "../util" }
anyhow = "1.0.38"
-async-broadcast = "0.3.4"
+async-broadcast = "0.4"
async-trait = "0.1"
futures = "0.3"
lazy_static = "1.4"
diff --git a/crates/live_kit/Cargo.toml b/crates/live_kit/Cargo.toml
deleted file mode 100644
index e88d4f7b24ccd21f9dd24480e031e01ca6078c31..0000000000000000000000000000000000000000
--- a/crates/live_kit/Cargo.toml
+++ /dev/null
@@ -1,22 +0,0 @@
-[package]
-name = "live_kit"
-version = "0.1.0"
-edition = "2021"
-description = "Bindings to LiveKit Swift client SDK"
-
-[lib]
-path = "src/live_kit.rs"
-doctest = false
-
-[dependencies]
-media = { path = "../media" }
-
-anyhow = "1.0.38"
-core-foundation = "0.9.3"
-core-graphics = "0.22.3"
-futures = "0.3"
-parking_lot = "0.11.1"
-
-[build-dependencies]
-serde = { version = "1.0", features = ["derive", "rc"] }
-serde_json = { version = "1.0", features = ["preserve_order"] }
diff --git a/crates/live_kit/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift b/crates/live_kit/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift
deleted file mode 100644
index f59b82920376e3ab9ffb11aa11ed50f151305079..0000000000000000000000000000000000000000
--- a/crates/live_kit/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift
+++ /dev/null
@@ -1,105 +0,0 @@
-import Foundation
-import LiveKit
-import WebRTC
-
-class LKRoomDelegate: RoomDelegate {
- var data: UnsafeRawPointer
- var onDidSubscribeToRemoteVideoTrack: @convention(c) (UnsafeRawPointer, UnsafeRawPointer) -> Void
-
- init(data: UnsafeRawPointer, onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer) -> Void) {
- self.data = data
- self.onDidSubscribeToRemoteVideoTrack = onDidSubscribeToRemoteVideoTrack
- }
-
- func room(_ room: Room, participant: RemoteParticipant, didSubscribe publication: RemoteTrackPublication, track: Track) {
- if track.kind == .video {
- self.onDidSubscribeToRemoteVideoTrack(self.data, Unmanaged.passRetained(track).toOpaque())
- }
- }
-}
-
-class LKVideoRenderer: NSObject, VideoRenderer {
- var data: UnsafeRawPointer
- var onFrame: @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Void
- var onDrop: @convention(c) (UnsafeRawPointer) -> Void
- var adaptiveStreamIsEnabled: Bool = false
- var adaptiveStreamSize: CGSize = .zero
-
- init(data: UnsafeRawPointer, onFrame: @escaping @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Void, onDrop: @escaping @convention(c) (UnsafeRawPointer) -> Void) {
- self.data = data
- self.onFrame = onFrame
- self.onDrop = onDrop
- }
-
- deinit {
- self.onDrop(self.data)
- }
-
- func setSize(_ size: CGSize) {
- print("Called setSize", size);
- }
-
- func renderFrame(_ frame: RTCVideoFrame?) {
- let buffer = frame?.buffer as? RTCCVPixelBuffer
- if let pixelBuffer = buffer?.pixelBuffer {
- self.onFrame(self.data, pixelBuffer)
- }
- }
-}
-
-@_cdecl("LKRelease")
-public func LKRelease(ptr: UnsafeRawPointer) {
- let _ = Unmanaged.fromOpaque(ptr).takeRetainedValue()
-}
-
-@_cdecl("LKRoomDelegateCreate")
-public func LKRoomDelegateCreate(data: UnsafeRawPointer, onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer) -> Void) -> UnsafeMutableRawPointer {
- let delegate = LKRoomDelegate(data: data, onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack)
- return Unmanaged.passRetained(delegate).toOpaque()
-}
-
-@_cdecl("LKRoomCreate")
-public func LKRoomCreate(delegate: UnsafeRawPointer) -> UnsafeMutableRawPointer {
- let delegate = Unmanaged.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.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("LKRoomPublishVideoTrack")
-public func LKRoomPublishVideoTrack(room: UnsafeRawPointer, track: UnsafeRawPointer, callback: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void, callback_data: UnsafeRawPointer) {
- let room = Unmanaged.fromOpaque(room).takeUnretainedValue()
- let track = Unmanaged.fromOpaque(track).takeUnretainedValue()
- room.localParticipant?.publishVideoTrack(track: track).then { _ in
- callback(callback_data, UnsafeRawPointer(nil) as! CFString?)
- }.catch { error in
- callback(callback_data, error.localizedDescription as CFString)
- }
-}
-
-@_cdecl("LKCreateScreenShareTrackForWindow")
-public func LKCreateScreenShareTrackForWindow(windowId: uint32) -> UnsafeMutableRawPointer {
- let track = LocalVideoTrack.createMacOSScreenShareTrack(source: .window(id: windowId))
- return Unmanaged.passRetained(track).toOpaque()
-}
-
-@_cdecl("LKVideoRendererCreate")
-public func LKVideoRendererCreate(data: UnsafeRawPointer, onFrame: @escaping @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Void, 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.fromOpaque(track).takeUnretainedValue() as! VideoTrack
- let renderer = Unmanaged.fromOpaque(renderer).takeRetainedValue()
- track.add(videoRenderer: renderer)
-}
diff --git a/crates/live_kit/src/live_kit.rs b/crates/live_kit/src/live_kit.rs
deleted file mode 100644
index 59ce860a7825108ac99aa8296e29fbd1a9876f14..0000000000000000000000000000000000000000
--- a/crates/live_kit/src/live_kit.rs
+++ /dev/null
@@ -1,276 +0,0 @@
-use anyhow::{anyhow, Context, Result};
-use core_foundation::{
- array::CFArray,
- base::{TCFType, TCFTypeRef},
- dictionary::CFDictionary,
- number::CFNumber,
- string::{CFString, CFStringRef},
-};
-use core_graphics::window::{
- kCGNullWindowID, kCGWindowListOptionExcludeDesktopElements, kCGWindowListOptionOnScreenOnly,
- kCGWindowNumber, kCGWindowOwnerName, kCGWindowOwnerPID, CGWindowListCopyWindowInfo,
-};
-use futures::{
- channel::{mpsc, oneshot},
- Future,
-};
-use media::core_video::{CVImageBuffer, CVImageBufferRef};
-use parking_lot::Mutex;
-use std::{
- ffi::c_void,
- sync::{Arc, Weak},
-};
-
-extern "C" {
- fn LKRelease(object: *const c_void);
-
- fn LKRoomDelegateCreate(
- callback_data: *mut c_void,
- on_did_subscribe_to_remote_video_track: extern "C" fn(
- callback_data: *mut c_void,
- remote_track: *const c_void,
- ),
- ) -> *const c_void;
-
- fn LKRoomCreate(delegate: *const c_void) -> *const c_void;
- fn LKRoomConnect(
- room: *const c_void,
- url: CFStringRef,
- token: CFStringRef,
- callback: extern "C" fn(*mut c_void, CFStringRef),
- callback_data: *mut c_void,
- );
- fn LKRoomPublishVideoTrack(
- room: *const c_void,
- track: *const c_void,
- callback: extern "C" fn(*mut c_void, CFStringRef),
- callback_data: *mut c_void,
- );
-
- fn LKVideoRendererCreate(
- callback_data: *mut c_void,
- on_frame: extern "C" fn(callback_data: *mut c_void, frame: CVImageBufferRef),
- on_drop: extern "C" fn(callback_data: *mut c_void),
- ) -> *const c_void;
-
- fn LKVideoTrackAddRenderer(track: *const c_void, renderer: *const c_void);
-
- fn LKCreateScreenShareTrackForWindow(windowId: u32) -> *const c_void;
-}
-
-pub struct Room {
- native_room: *const c_void,
- remote_video_track_subscribers: Mutex>>>,
- _delegate: RoomDelegate,
-}
-
-impl Room {
- pub fn new() -> Arc {
- Arc::new_cyclic(|weak_room| {
- let delegate = RoomDelegate::new(weak_room.clone());
- Self {
- native_room: unsafe { LKRoomCreate(delegate.native_delegate) },
- remote_video_track_subscribers: Default::default(),
- _delegate: delegate,
- }
- })
- }
-
- pub fn connect(&self, url: &str, token: &str) -> impl Future> {
- 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,
- )
- }
-
- async { rx.await.unwrap().context("error connecting to room") }
- }
-
- pub fn publish_video_track(&self, track: &LocalVideoTrack) -> impl Future> {
- let (did_publish, tx, rx) = Self::build_done_callback();
- unsafe {
- LKRoomPublishVideoTrack(self.native_room, track.0, did_publish, tx);
- }
- async { rx.await.unwrap().context("error publishing video track") }
- }
-
- pub fn remote_video_tracks(&self) -> mpsc::UnboundedReceiver> {
- let (tx, rx) = mpsc::unbounded();
- self.remote_video_track_subscribers.lock().push(tx);
- rx
- }
-
- fn did_subscribe_to_remote_video_track(&self, track: RemoteVideoTrack) {
- let track = Arc::new(track);
- self.remote_video_track_subscribers
- .lock()
- .retain(|tx| tx.unbounded_send(track.clone()).is_ok());
- }
-
- fn build_done_callback() -> (
- extern "C" fn(*mut c_void, CFStringRef),
- *mut c_void,
- oneshot::Receiver>,
- ) {
- 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>) };
- 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,
- )
- }
-}
-
-impl Drop for Room {
- fn drop(&mut self) {
- unsafe { LKRelease(self.native_room) }
- }
-}
-
-struct RoomDelegate {
- native_delegate: *const c_void,
- weak_room: *const Room,
-}
-
-impl RoomDelegate {
- fn new(weak_room: Weak) -> Self {
- let weak_room = Weak::into_raw(weak_room);
- let native_delegate = unsafe {
- LKRoomDelegateCreate(
- weak_room as *mut c_void,
- Self::on_did_subscribe_to_remote_video_track,
- )
- };
- Self {
- native_delegate,
- weak_room,
- }
- }
-
- extern "C" fn on_did_subscribe_to_remote_video_track(room: *mut c_void, track: *const c_void) {
- let room = unsafe { Weak::from_raw(room as *mut Room) };
- let track = RemoteVideoTrack(track);
- if let Some(room) = room.upgrade() {
- room.did_subscribe_to_remote_video_track(track);
- }
- let _ = Weak::into_raw(room);
- }
-}
-
-impl Drop for RoomDelegate {
- fn drop(&mut self) {
- unsafe {
- LKRelease(self.native_delegate);
- let _ = Weak::from_raw(self.weak_room);
- }
- }
-}
-
-pub struct LocalVideoTrack(*const c_void);
-
-impl LocalVideoTrack {
- pub fn screen_share_for_window(window_id: u32) -> Self {
- Self(unsafe { LKCreateScreenShareTrackForWindow(window_id) })
- }
-}
-
-impl Drop for LocalVideoTrack {
- fn drop(&mut self) {
- unsafe { LKRelease(self.0) }
- }
-}
-
-pub struct RemoteVideoTrack(*const c_void);
-
-impl RemoteVideoTrack {
- pub fn add_renderer(&self, callback: F)
- where
- F: 'static + FnMut(CVImageBuffer),
- {
- extern "C" fn on_frame(callback_data: *mut c_void, frame: CVImageBufferRef)
- where
- F: FnMut(CVImageBuffer),
- {
- unsafe {
- let buffer = CVImageBuffer::wrap_under_get_rule(frame);
- let callback = &mut *(callback_data as *mut F);
- callback(buffer);
- }
- }
-
- extern "C" fn on_drop(callback_data: *mut c_void) {
- unsafe {
- let _ = Box::from_raw(callback_data as *mut F);
- }
- }
-
- let callback_data = Box::into_raw(Box::new(callback));
- unsafe {
- let renderer =
- LKVideoRendererCreate(callback_data as *mut c_void, on_frame::, on_drop::);
- LKVideoTrackAddRenderer(self.0, renderer);
- }
- }
-}
-
-impl Drop for RemoteVideoTrack {
- fn drop(&mut self) {
- unsafe { LKRelease(self.0) }
- }
-}
-
-#[derive(Debug)]
-pub struct WindowInfo {
- pub id: u32,
- pub owner_pid: i32,
- pub owner_name: Option,
-}
-
-pub fn list_windows() -> Vec {
- unsafe {
- let dicts = CFArray::::wrap_under_get_rule(CGWindowListCopyWindowInfo(
- kCGWindowListOptionOnScreenOnly | kCGWindowListOptionExcludeDesktopElements,
- kCGNullWindowID,
- ));
-
- dicts
- .iter()
- .map(|dict| {
- let id =
- CFNumber::wrap_under_get_rule(*dict.get(kCGWindowNumber.as_void_ptr()) as _)
- .to_i64()
- .unwrap() as u32;
-
- let owner_pid =
- CFNumber::wrap_under_get_rule(*dict.get(kCGWindowOwnerPID.as_void_ptr()) as _)
- .to_i32()
- .unwrap();
-
- let owner_name = dict
- .find(kCGWindowOwnerName.as_void_ptr())
- .map(|name| CFString::wrap_under_get_rule(*name as _).to_string());
- WindowInfo {
- id,
- owner_pid,
- owner_name,
- }
- })
- .collect()
- }
-}
diff --git a/crates/live_kit_client/.cargo/config.toml b/crates/live_kit_client/.cargo/config.toml
new file mode 100644
index 0000000000000000000000000000000000000000..b33fe211bd57624e6d2ccf0213f1372decd92b88
--- /dev/null
+++ b/crates/live_kit_client/.cargo/config.toml
@@ -0,0 +1,2 @@
+[live_kit_client_test]
+rustflags = ["-C", "link-args=-ObjC"]
diff --git a/crates/live_kit_client/Cargo.toml b/crates/live_kit_client/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..d0f54782b96d9a1ef4d823e32d4ccac4aad502fc
--- /dev/null
+++ b/crates/live_kit_client/Cargo.toml
@@ -0,0 +1,70 @@
+[package]
+name = "live_kit_client"
+version = "0.1.0"
+edition = "2021"
+description = "Bindings to LiveKit Swift client SDK"
+
+[lib]
+path = "src/live_kit_client.rs"
+doctest = false
+
+[[example]]
+name = "test_app"
+
+[features]
+test-support = [
+ "async-trait",
+ "collections/test-support",
+ "gpui/test-support",
+ "lazy_static",
+ "live_kit_server",
+ "nanoid",
+]
+
+[dependencies]
+collections = { path = "../collections", optional = true }
+gpui = { path = "../gpui", optional = true }
+live_kit_server = { path = "../live_kit_server", optional = true }
+media = { path = "../media" }
+
+anyhow = "1.0.38"
+async-broadcast = "0.4"
+core-foundation = "0.9.3"
+core-graphics = "0.22.3"
+futures = "0.3"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
+parking_lot = "0.11.1"
+postage = { version = "0.4.1", features = ["futures-traits"] }
+
+async-trait = { version = "0.1", optional = true }
+lazy_static = { version = "1.4", optional = true }
+nanoid = { version ="0.4", optional = true}
+
+[dev-dependencies]
+collections = { path = "../collections", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }
+live_kit_server = { path = "../live_kit_server" }
+media = { path = "../media" }
+
+anyhow = "1.0.38"
+async-trait = "0.1"
+block = "0.1"
+bytes = "1.2"
+byteorder = "1.4"
+cocoa = "0.24"
+core-foundation = "0.9.3"
+core-graphics = "0.22.3"
+foreign-types = "0.3"
+futures = "0.3"
+hmac = "0.12"
+jwt = "0.16"
+lazy_static = "1.4"
+objc = "0.2"
+parking_lot = "0.11.1"
+serde = { version = "1.0", features = ["derive", "rc"] }
+sha2 = "0.10"
+simplelog = "0.9"
+
+[build-dependencies]
+serde = { version = "1.0", features = ["derive", "rc"] }
+serde_json = { version = "1.0", features = ["preserve_order"] }
diff --git a/crates/live_kit/LiveKitBridge/.gitignore b/crates/live_kit_client/LiveKitBridge/.gitignore
similarity index 100%
rename from crates/live_kit/LiveKitBridge/.gitignore
rename to crates/live_kit_client/LiveKitBridge/.gitignore
diff --git a/crates/live_kit/LiveKitBridge/Package.resolved b/crates/live_kit_client/LiveKitBridge/Package.resolved
similarity index 81%
rename from crates/live_kit/LiveKitBridge/Package.resolved
rename to crates/live_kit_client/LiveKitBridge/Package.resolved
index b19e2980a493bd24b9f94f4811bdda08fa4f4a40..9318cc01847dd6d793b17dd869dc60d9f2aa55cd 100644
--- a/crates/live_kit/LiveKitBridge/Package.resolved
+++ b/crates/live_kit_client/LiveKitBridge/Package.resolved
@@ -6,7 +6,7 @@
"repositoryURL": "https://github.com/livekit/client-sdk-swift.git",
"state": {
"branch": null,
- "revision": "5cc3c001779ab147199ce3ea0dce465b846368b4",
+ "revision": "f6ca534eb334e99acb8e82cc99b491717df28d8a",
"version": null
}
},
@@ -24,8 +24,8 @@
"repositoryURL": "https://github.com/webrtc-sdk/Specs.git",
"state": {
"branch": null,
- "revision": "5225f2de4b6d0098803b3a0e55b255a41f293dad",
- "version": "104.5112.2"
+ "revision": "38ac06261e62f980652278c69b70284324c769e0",
+ "version": "104.5112.5"
}
},
{
@@ -42,8 +42,8 @@
"repositoryURL": "https://github.com/apple/swift-protobuf.git",
"state": {
"branch": null,
- "revision": "b8230909dedc640294d7324d37f4c91ad3dcf177",
- "version": "1.20.1"
+ "revision": "88c7d15e1242fdb6ecbafbc7926426a19be1e98a",
+ "version": "1.20.2"
}
}
]
diff --git a/crates/live_kit/LiveKitBridge/Package.swift b/crates/live_kit_client/LiveKitBridge/Package.swift
similarity index 93%
rename from crates/live_kit/LiveKitBridge/Package.swift
rename to crates/live_kit_client/LiveKitBridge/Package.swift
index 76e528bda98a2c9495256d2a2f57e612d4480f21..bdd664c6fbf0d07d2b4408a99209410f5d7a656a 100644
--- a/crates/live_kit/LiveKitBridge/Package.swift
+++ b/crates/live_kit_client/LiveKitBridge/Package.swift
@@ -15,7 +15,7 @@ let package = Package(
targets: ["LiveKitBridge"]),
],
dependencies: [
- .package(url: "https://github.com/livekit/client-sdk-swift.git", revision: "5cc3c001779ab147199ce3ea0dce465b846368b4"),
+ .package(url: "https://github.com/livekit/client-sdk-swift.git", revision: "f6ca534eb334e99acb8e82cc99b491717df28d8a"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
diff --git a/crates/live_kit/LiveKitBridge/README.md b/crates/live_kit_client/LiveKitBridge/README.md
similarity index 100%
rename from crates/live_kit/LiveKitBridge/README.md
rename to crates/live_kit_client/LiveKitBridge/README.md
diff --git a/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift b/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift
new file mode 100644
index 0000000000000000000000000000000000000000..a0326b24a1c480d55bc307b178f6562cba1b3d07
--- /dev/null
+++ b/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift
@@ -0,0 +1,179 @@
+import Foundation
+import LiveKit
+import WebRTC
+import ScreenCaptureKit
+
+class LKRoomDelegate: RoomDelegate {
+ var data: UnsafeRawPointer
+ var onDidDisconnect: @convention(c) (UnsafeRawPointer) -> Void
+ var onDidSubscribeToRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void
+ var onDidUnsubscribeFromRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
+
+ init(
+ data: UnsafeRawPointer,
+ onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void,
+ onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
+ onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void)
+ {
+ self.data = data
+ self.onDidDisconnect = onDidDisconnect
+ self.onDidSubscribeToRemoteVideoTrack = onDidSubscribeToRemoteVideoTrack
+ self.onDidUnsubscribeFromRemoteVideoTrack = onDidUnsubscribeFromRemoteVideoTrack
+ }
+
+ 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())
+ }
+ }
+
+ 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)
+ }
+ }
+}
+
+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,
+ onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
+ onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
+) -> UnsafeMutableRawPointer {
+ let delegate = LKRoomDelegate(
+ data: data,
+ onDidDisconnect: onDidDisconnect,
+ onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack,
+ onDidUnsubscribeFromRemoteVideoTrack: onDidUnsubscribeFromRemoteVideoTrack
+ )
+ return Unmanaged.passRetained(delegate).toOpaque()
+}
+
+@_cdecl("LKRoomCreate")
+public func LKRoomCreate(delegate: UnsafeRawPointer) -> UnsafeMutableRawPointer {
+ let delegate = Unmanaged.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.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.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.fromOpaque(room).takeUnretainedValue()
+ let track = Unmanaged.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("LKRoomUnpublishTrack")
+public func LKRoomUnpublishTrack(room: UnsafeRawPointer, publication: UnsafeRawPointer) {
+ let room = Unmanaged.fromOpaque(room).takeUnretainedValue()
+ let publication = Unmanaged.fromOpaque(publication).takeUnretainedValue()
+ let _ = room.localParticipant?.unpublish(publication: publication)
+}
+
+@_cdecl("LKRoomVideoTracksForRemoteParticipant")
+public func LKRoomVideoTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
+ let room = Unmanaged.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("LKCreateScreenShareTrackForDisplay")
+public func LKCreateScreenShareTrackForDisplay(display: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer {
+ let display = Unmanaged.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.fromOpaque(track).takeUnretainedValue() as! VideoTrack
+ let renderer = Unmanaged.fromOpaque(renderer).takeRetainedValue()
+ renderer.track = track
+ track.add(videoRenderer: renderer)
+}
+
+@_cdecl("LKRemoteVideoTrackGetSid")
+public func LKRemoteVideoTrackGetSid(track: UnsafeRawPointer) -> CFString {
+ let track = Unmanaged.fromOpaque(track).takeUnretainedValue()
+ return track.sid! as CFString
+}
+
+@_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)
+ }
+}
diff --git a/crates/live_kit/build.rs b/crates/live_kit_client/build.rs
similarity index 78%
rename from crates/live_kit/build.rs
rename to crates/live_kit_client/build.rs
index 79d7d84cdd96c63381e4ec9b29fb30e28b788e05..bceb4fb927da826920cab1af91b67a92325ea8a5 100644
--- a/crates/live_kit/build.rs
+++ b/crates/live_kit_client/build.rs
@@ -32,17 +32,23 @@ pub struct SwiftTarget {
pub paths: SwiftPaths,
}
-const MACOS_TARGET_VERSION: &str = "10.15";
+const MACOS_TARGET_VERSION: &str = "10.15.7";
fn main() {
- let swift_target = get_swift_target();
+ if cfg!(not(any(test, feature = "test-support"))) {
+ let swift_target = get_swift_target();
- build_bridge(&swift_target);
- link_swift_stdlib(&swift_target);
- link_webrtc_framework(&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",
@@ -76,13 +82,9 @@ fn build_bridge(swift_target: &SwiftTarget) {
}
fn link_swift_stdlib(swift_target: &SwiftTarget) {
- swift_target
- .paths
- .runtime_library_paths
- .iter()
- .for_each(|path| {
- println!("cargo:rustc-link-search=native={}", path);
- });
+ for path in &swift_target.paths.runtime_library_paths {
+ println!("cargo:rustc-link-search=native={}", path);
+ }
}
fn link_webrtc_framework(swift_target: &SwiftTarget) {
@@ -94,6 +96,8 @@ fn link_webrtc_framework(swift_target: &SwiftTarget) {
);
// 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 =
@@ -125,9 +129,20 @@ fn swift_package_root() -> PathBuf {
}
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")
+ .arg("-R")
.args(&[source, destination])
.status()
.unwrap()
diff --git a/crates/live_kit_client/examples/test_app.rs b/crates/live_kit_client/examples/test_app.rs
new file mode 100644
index 0000000000000000000000000000000000000000..eddee785bc2ac3c4572d3ab61ea6c567fbea602a
--- /dev/null
+++ b/crates/live_kit_client/examples/test_app.rs
@@ -0,0 +1,93 @@
+use futures::StreamExt;
+use gpui::{actions, keymap::Binding, Menu, MenuItem};
+use live_kit_client::{LocalVideoTrack, RemoteVideoTrackUpdate, Room};
+use live_kit_server::token::{self, VideoGrant};
+use log::LevelFilter;
+use simplelog::SimpleLogger;
+
+actions!(capture, [Quit]);
+
+fn main() {
+ SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
+
+ gpui::App::new(()).unwrap().run(|cx| {
+ cx.platform().activate(true);
+ cx.add_global_action(quit);
+
+ cx.add_bindings([Binding::new("cmd-q", Quit, None)]);
+ cx.set_menus(vec![Menu {
+ name: "Zed",
+ items: vec![MenuItem::Action {
+ name: "Quit",
+ action: Box::new(Quit),
+ }],
+ }]);
+
+ 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());
+
+ cx.spawn(|cx| async move {
+ let user_a_token = token::create(
+ &live_kit_key,
+ &live_kit_secret,
+ Some("test-participant-1"),
+ VideoGrant::to_join("test-room"),
+ )
+ .unwrap();
+ 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();
+ let room_b = Room::new();
+ room_b.connect(&live_kit_url, &user2_token).await.unwrap();
+
+ let mut track_changes = room_b.remote_video_track_updates();
+
+ let displays = room_a.display_sources().await.unwrap();
+ let display = displays.into_iter().next().unwrap();
+
+ let track_a = LocalVideoTrack::screen_share_for_display(&display);
+ let track_a_publication = room_a.publish_video_track(&track_a).await.unwrap();
+
+ if let RemoteVideoTrackUpdate::Subscribed(track) = track_changes.next().await.unwrap() {
+ let remote_tracks = room_b.remote_video_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");
+ }
+
+ let remote_track = room_b
+ .remote_video_tracks("test-participant-1")
+ .pop()
+ .unwrap();
+ room_a.unpublish_track(track_a_publication);
+ if let RemoteVideoTrackUpdate::Unsubscribed {
+ publisher_id,
+ track_id,
+ } = track_changes.next().await.unwrap()
+ {
+ assert_eq!(publisher_id, "test-participant-1");
+ assert_eq!(remote_track.sid(), track_id);
+ assert_eq!(room_b.remote_video_tracks("test-participant-1").len(), 0);
+ } else {
+ panic!("unexpected message");
+ }
+
+ cx.platform().quit();
+ })
+ .detach();
+ });
+}
+
+fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
+ cx.platform().quit();
+}
diff --git a/crates/live_kit_client/src/live_kit_client.rs b/crates/live_kit_client/src/live_kit_client.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2ded57082801428e35ff5ce87d1b8740db0fffb7
--- /dev/null
+++ b/crates/live_kit_client/src/live_kit_client.rs
@@ -0,0 +1,10 @@
+pub mod prod;
+
+#[cfg(not(any(test, feature = "test-support")))]
+pub use prod::*;
+
+#[cfg(any(test, feature = "test-support"))]
+mod test;
+
+#[cfg(any(test, feature = "test-support"))]
+pub use test::*;
diff --git a/crates/live_kit_client/src/prod.rs b/crates/live_kit_client/src/prod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..47fd4f0b69d0f3b40702888c1953e0aa39f54af3
--- /dev/null
+++ b/crates/live_kit_client/src/prod.rs
@@ -0,0 +1,493 @@
+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},
+};
+
+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_video_track: extern "C" fn(
+ callback_data: *mut c_void,
+ publisher_id: CFStringRef,
+ track_id: CFStringRef,
+ remote_track: *const c_void,
+ ),
+ on_did_unsubscribe_from_remote_video_track: extern "C" fn(
+ callback_data: *mut c_void,
+ publisher_id: CFStringRef,
+ track_id: CFStringRef,
+ ),
+ ) -> *const c_void;
+
+ fn LKRoomCreate(delegate: *const c_void) -> *const c_void;
+ fn LKRoomConnect(
+ room: *const c_void,
+ url: CFStringRef,
+ token: CFStringRef,
+ callback: extern "C" fn(*mut c_void, CFStringRef),
+ callback_data: *mut c_void,
+ );
+ fn LKRoomDisconnect(room: *const c_void);
+ fn LKRoomPublishVideoTrack(
+ room: *const c_void,
+ track: *const c_void,
+ callback: extern "C" fn(*mut c_void, *mut c_void, CFStringRef),
+ callback_data: *mut c_void,
+ );
+ fn LKRoomUnpublishTrack(room: *const c_void, publication: *const c_void);
+ fn LKRoomVideoTracksForRemoteParticipant(
+ room: *const c_void,
+ 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 LKVideoTrackAddRenderer(track: *const c_void, renderer: *const c_void);
+ fn LKRemoteVideoTrackGetSid(track: *const c_void) -> CFStringRef;
+
+ fn LKDisplaySources(
+ callback_data: *mut c_void,
+ callback: extern "C" fn(
+ callback_data: *mut c_void,
+ sources: CFArrayRef,
+ error: CFStringRef,
+ ),
+ );
+ fn LKCreateScreenShareTrackForDisplay(display: *const c_void) -> *const c_void;
+}
+
+pub type Sid = String;
+
+#[derive(Clone, Eq, PartialEq)]
+pub enum ConnectionState {
+ Disconnected,
+ Connected { url: String, token: String },
+}
+
+pub struct Room {
+ native_room: *const c_void,
+ connection: Mutex<(
+ watch::Sender,
+ watch::Receiver,
+ )>,
+ remote_video_track_subscribers: Mutex>>,
+ _delegate: RoomDelegate,
+}
+
+impl Room {
+ pub fn new() -> Arc {
+ 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)),
+ remote_video_track_subscribers: Default::default(),
+ _delegate: delegate,
+ }
+ })
+ }
+
+ pub fn status(&self) -> watch::Receiver {
+ self.connection.lock().1.clone()
+ }
+
+ pub fn connect(self: &Arc, url: &str, token: &str) -> impl Future> {
+ 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 {
+ match rx.await.unwrap().context("error connecting to room") {
+ Ok(()) => {
+ *this.connection.lock().0.borrow_mut() =
+ ConnectionState::Connected { url, token };
+ Ok(())
+ }
+ Err(err) => Err(err),
+ }
+ }
+ }
+
+ fn did_disconnect(&self) {
+ *self.connection.lock().0.borrow_mut() = ConnectionState::Disconnected;
+ }
+
+ pub fn display_sources(self: &Arc) -> impl Future>> {
+ extern "C" fn callback(tx: *mut c_void, sources: CFArrayRef, error: CFStringRef) {
+ unsafe {
+ let tx = Box::from_raw(tx as *mut oneshot::Sender>>);
+
+ 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(*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,
+ track: &LocalVideoTrack,
+ ) -> impl Future> {
+ let (tx, rx) = oneshot::channel::>();
+ extern "C" fn callback(tx: *mut c_void, publication: *mut c_void, error: CFStringRef) {
+ let tx =
+ unsafe { Box::from_raw(tx as *mut oneshot::Sender>) };
+ if error.is_null() {
+ let _ = tx.send(Ok(LocalTrackPublication(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 unpublish_track(&self, publication: LocalTrackPublication) {
+ unsafe {
+ LKRoomUnpublishTrack(self.native_room, publication.0);
+ }
+ }
+
+ pub fn remote_video_tracks(&self, participant_id: &str) -> Vec> {
+ 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 = *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_video_track_updates(&self) -> mpsc::UnboundedReceiver {
+ let (tx, rx) = mpsc::unbounded();
+ self.remote_video_track_subscribers.lock().push(tx);
+ rx
+ }
+
+ fn did_subscribe_to_remote_video_track(&self, track: RemoteVideoTrack) {
+ let track = Arc::new(track);
+ self.remote_video_track_subscribers.lock().retain(|tx| {
+ tx.unbounded_send(RemoteVideoTrackUpdate::Subscribed(track.clone()))
+ .is_ok()
+ });
+ }
+
+ fn did_unsubscribe_from_remote_video_track(&self, publisher_id: String, track_id: String) {
+ self.remote_video_track_subscribers.lock().retain(|tx| {
+ tx.unbounded_send(RemoteVideoTrackUpdate::Unsubscribed {
+ 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>,
+ ) {
+ 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>) };
+ 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,
+ )
+ }
+}
+
+impl Drop for Room {
+ fn drop(&mut self) {
+ unsafe {
+ LKRoomDisconnect(self.native_room);
+ CFRelease(self.native_room);
+ }
+ }
+}
+
+struct RoomDelegate {
+ native_delegate: *const c_void,
+ weak_room: *const Room,
+}
+
+impl RoomDelegate {
+ fn new(weak_room: Weak) -> Self {
+ let weak_room = Weak::into_raw(weak_room);
+ let native_delegate = unsafe {
+ LKRoomDelegateCreate(
+ weak_room as *mut c_void,
+ Self::on_did_disconnect,
+ Self::on_did_subscribe_to_remote_video_track,
+ Self::on_did_unsubscribe_from_remote_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_video_track(
+ room: *mut c_void,
+ publisher_id: CFStringRef,
+ track_id: CFStringRef,
+ track: *const c_void,
+ ) {
+ 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);
+ }
+}
+
+impl Drop for RoomDelegate {
+ fn drop(&mut self) {
+ unsafe {
+ CFRelease(self.native_delegate);
+ let _ = Weak::from_raw(self.weak_room);
+ }
+ }
+}
+
+pub struct LocalVideoTrack(*const c_void);
+
+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) }
+ }
+}
+
+pub struct LocalTrackPublication(*const c_void);
+
+impl Drop for LocalTrackPublication {
+ fn drop(&mut self) {
+ unsafe { CFRelease(self.0) }
+ }
+}
+
+#[derive(Debug)]
+pub struct RemoteVideoTrack {
+ native_track: *const c_void,
+ sid: Sid,
+ publisher_id: String,
+}
+
+impl RemoteVideoTrack {
+ fn new(native_track: *const c_void, sid: Sid, publisher_id: String) -> Self {
+ unsafe {
+ CFRetain(native_track);
+ }
+ 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 {
+ 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 );
+ 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 );
+ }
+ }
+
+ 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) }
+ }
+}
+
+pub enum RemoteVideoTrackUpdate {
+ Subscribed(Arc),
+ Unsubscribed { publisher_id: Sid, track_id: Sid },
+}
+
+pub struct MacOSDisplay(*const c_void);
+
+impl MacOSDisplay {
+ fn new(ptr: *const c_void) -> Self {
+ unsafe {
+ CFRetain(ptr);
+ }
+ Self(ptr)
+ }
+}
+
+impl Drop for MacOSDisplay {
+ fn drop(&mut self) {
+ unsafe { CFRelease(self.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()
+ }
+}
diff --git a/crates/live_kit_client/src/test.rs b/crates/live_kit_client/src/test.rs
new file mode 100644
index 0000000000000000000000000000000000000000..329e4e117625d20a64cb5f675dfee9f20de5b6df
--- /dev/null
+++ b/crates/live_kit_client/src/test.rs
@@ -0,0 +1,433 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use collections::HashMap;
+use futures::Stream;
+use gpui::executor::Background;
+use lazy_static::lazy_static;
+use live_kit_server::token;
+use media::core_video::CVImageBuffer;
+use parking_lot::Mutex;
+use postage::watch;
+use std::{future::Future, mem, sync::Arc};
+
+lazy_static! {
+ static ref SERVERS: Mutex>> = Default::default();
+}
+
+pub struct TestServer {
+ pub url: String,
+ pub api_key: String,
+ pub secret_key: String,
+ rooms: Mutex>,
+ background: Arc,
+}
+
+impl TestServer {
+ pub fn create(
+ url: String,
+ api_key: String,
+ secret_key: String,
+ background: Arc,
+ ) -> Result> {
+ let mut servers = SERVERS.lock();
+ if servers.contains_key(&url) {
+ Err(anyhow!("a server with url {:?} already exists", url))
+ } else {
+ let server = Arc::new(TestServer {
+ url: url.clone(),
+ api_key,
+ secret_key,
+ rooms: Default::default(),
+ background,
+ });
+ servers.insert(url, server.clone());
+ Ok(server)
+ }
+ }
+
+ fn get(url: &str) -> Result> {
+ Ok(SERVERS
+ .lock()
+ .get(url)
+ .ok_or_else(|| anyhow!("no server found for url"))?
+ .clone())
+ }
+
+ pub fn teardown(&self) -> Result<()> {
+ SERVERS
+ .lock()
+ .remove(&self.url)
+ .ok_or_else(|| anyhow!("server with url {:?} does not exist", self.url))?;
+ Ok(())
+ }
+
+ pub fn create_api_client(&self) -> TestApiClient {
+ TestApiClient {
+ url: self.url.clone(),
+ }
+ }
+
+ async fn create_room(&self, room: String) -> Result<()> {
+ self.background.simulate_random_delay().await;
+ let mut server_rooms = self.rooms.lock();
+ if server_rooms.contains_key(&room) {
+ Err(anyhow!("room {:?} already exists", room))
+ } else {
+ server_rooms.insert(room, Default::default());
+ Ok(())
+ }
+ }
+
+ async fn delete_room(&self, room: String) -> Result<()> {
+ // TODO: clear state associated with all `Room`s.
+ self.background.simulate_random_delay().await;
+ let mut server_rooms = self.rooms.lock();
+ server_rooms
+ .remove(&room)
+ .ok_or_else(|| anyhow!("room {:?} does not exist", room))?;
+ Ok(())
+ }
+
+ async fn join_room(&self, token: String, client_room: Arc) -> Result<()> {
+ self.background.simulate_random_delay().await;
+ let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
+ 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
+ .get_mut(&*room_name)
+ .ok_or_else(|| anyhow!("room {:?} does not exist", room_name))?;
+ if room.client_rooms.contains_key(&identity) {
+ Err(anyhow!(
+ "{:?} attempted to join room {:?} twice",
+ identity,
+ room_name
+ ))
+ } else {
+ room.client_rooms.insert(identity, client_room);
+ Ok(())
+ }
+ }
+
+ async fn leave_room(&self, token: String) -> Result<()> {
+ self.background.simulate_random_delay().await;
+ let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
+ 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
+ .get_mut(&*room_name)
+ .ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
+ room.client_rooms.remove(&identity).ok_or_else(|| {
+ anyhow!(
+ "{:?} attempted to leave room {:?} before joining it",
+ identity,
+ room_name
+ )
+ })?;
+ Ok(())
+ }
+
+ async fn remove_participant(&self, room_name: String, identity: String) -> Result<()> {
+ // TODO: clear state associated with the `Room`.
+
+ self.background.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.client_rooms.remove(&identity).ok_or_else(|| {
+ anyhow!(
+ "participant {:?} did not join room {:?}",
+ identity,
+ room_name
+ )
+ })?;
+ Ok(())
+ }
+
+ pub async fn disconnect_client(&self, client_identity: String) {
+ self.background.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) {
+ *room.0.lock().connection.0.borrow_mut() = ConnectionState::Disconnected;
+ }
+ }
+ }
+
+ async fn publish_video_track(&self, token: String, local_track: LocalVideoTrack) -> Result<()> {
+ self.background.simulate_random_delay().await;
+ let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
+ 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
+ .get_mut(&*room_name)
+ .ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
+
+ let update = RemoteVideoTrackUpdate::Subscribed(Arc::new(RemoteVideoTrack {
+ sid: nanoid::nanoid!(17),
+ publisher_id: identity.clone(),
+ frames_rx: local_track.frames_rx.clone(),
+ }));
+
+ for (id, client_room) in &room.client_rooms {
+ if *id != identity {
+ let _ = client_room
+ .0
+ .lock()
+ .video_track_updates
+ .0
+ .try_broadcast(update.clone())
+ .unwrap();
+ }
+ }
+
+ Ok(())
+ }
+}
+
+#[derive(Default)]
+struct TestServerRoom {
+ client_rooms: HashMap>,
+}
+
+impl TestServerRoom {}
+
+pub struct TestApiClient {
+ url: String,
+}
+
+#[async_trait]
+impl live_kit_server::api::Client for TestApiClient {
+ fn url(&self) -> &str {
+ &self.url
+ }
+
+ async fn create_room(&self, name: String) -> Result<()> {
+ let server = TestServer::get(&self.url)?;
+ server.create_room(name).await?;
+ Ok(())
+ }
+
+ async fn delete_room(&self, name: String) -> Result<()> {
+ let server = TestServer::get(&self.url)?;
+ server.delete_room(name).await?;
+ Ok(())
+ }
+
+ async fn remove_participant(&self, room: String, identity: String) -> Result<()> {
+ let server = TestServer::get(&self.url)?;
+ server.remove_participant(room, identity).await?;
+ Ok(())
+ }
+
+ fn room_token(&self, room: &str, identity: &str) -> Result {
+ let server = TestServer::get(&self.url)?;
+ token::create(
+ &server.api_key,
+ &server.secret_key,
+ Some(identity),
+ token::VideoGrant::to_join(room),
+ )
+ }
+}
+
+pub type Sid = String;
+
+struct RoomState {
+ connection: (
+ watch::Sender,
+ watch::Receiver,
+ ),
+ display_sources: Vec,
+ video_track_updates: (
+ async_broadcast::Sender,
+ async_broadcast::Receiver,
+ ),
+}
+
+#[derive(Clone, Eq, PartialEq)]
+pub enum ConnectionState {
+ Disconnected,
+ Connected { url: String, token: String },
+}
+
+pub struct Room(Mutex);
+
+impl Room {
+ pub fn new() -> Arc {
+ Arc::new(Self(Mutex::new(RoomState {
+ connection: watch::channel_with(ConnectionState::Disconnected),
+ display_sources: Default::default(),
+ video_track_updates: async_broadcast::broadcast(128),
+ })))
+ }
+
+ pub fn status(&self) -> watch::Receiver {
+ self.0.lock().connection.1.clone()
+ }
+
+ pub fn connect(self: &Arc, url: &str, token: &str) -> impl Future> {
+ 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?;
+ *this.0.lock().connection.0.borrow_mut() = ConnectionState::Connected { url, token };
+ Ok(())
+ }
+ }
+
+ pub fn display_sources(self: &Arc) -> impl Future>> {
+ let this = self.clone();
+ async move {
+ let server = this.test_server();
+ server.background.simulate_random_delay().await;
+ Ok(this.0.lock().display_sources.clone())
+ }
+ }
+
+ pub fn publish_video_track(
+ self: &Arc,
+ track: &LocalVideoTrack,
+ ) -> impl Future> {
+ let this = self.clone();
+ let track = track.clone();
+ async move {
+ this.test_server()
+ .publish_video_track(this.token(), track)
+ .await?;
+ Ok(LocalTrackPublication)
+ }
+ }
+
+ pub fn unpublish_track(&self, _: LocalTrackPublication) {}
+
+ pub fn remote_video_tracks(&self, _: &str) -> Vec