WIP

Antonio Scandurra created

Change summary

Cargo.lock                                                                     |  26 
crates/call/Cargo.toml                                                         |   3 
crates/call/src/call.rs                                                        |   2 
crates/call/src/participant.rs                                                 |  20 
crates/call/src/room.rs                                                        | 111 
crates/capture/Cargo.toml                                                      |  29 
crates/capture/build.rs                                                        |   7 
crates/capture/src/main.rs                                                     | 150 
crates/collab/src/rpc.rs                                                       |  16 
crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift |  25 
crates/live_kit_client/src/live_kit_client.rs                                  |  85 
crates/live_kit_server/src/api.rs                                              |   2 
crates/workspace/src/pane_group.rs                                             |  32 
13 files changed, 245 insertions(+), 263 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -732,6 +732,7 @@ dependencies = [
  "futures 0.3.24",
  "gpui",
  "live_kit_client",
+ "media",
  "postage",
  "project",
  "util",
@@ -803,31 +804,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",
- "live_kit_client",
- "live_kit_server",
- "log",
- "media",
- "objc",
- "parking_lot 0.11.2",
- "postage",
- "simplelog",
-]
-
 [[package]]
 name = "castaway"
 version = "0.1.2"

crates/call/Cargo.toml 🔗

@@ -19,8 +19,9 @@ test-support = [
 [dependencies]
 client = { path = "../client" }
 collections = { path = "../collections" }
-live_kit_client = { path = "../live_kit_client" }
 gpui = { path = "../gpui" }
+live_kit_client = { path = "../live_kit_client" }
+media = { path = "../media" }
 project = { path = "../project" }
 util = { path = "../util" }
 

crates/call/src/call.rs 🔗

@@ -132,6 +132,8 @@ impl ActiveCall {
                         Room::create(recipient_user_id, initial_project, client, user_store, cx)
                     })
                     .await?;
+                room.update(&mut cx, |room, cx| room.share_screen(cx))
+                    .await?;
                 this.update(&mut cx, |this, cx| this.set_room(Some(room), cx));
             };
 

crates/call/src/participant.rs 🔗

@@ -1,6 +1,8 @@
 use anyhow::{anyhow, Result};
 use client::{proto, User};
-use gpui::WeakModelHandle;
+use collections::HashMap;
+use gpui::{Task, WeakModelHandle};
+use media::core_video::CVImageBuffer;
 use project::Project;
 use std::sync::Arc;
 
@@ -34,9 +36,23 @@ pub struct LocalParticipant {
     pub active_project: Option<WeakModelHandle<Project>>,
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone)]
 pub struct RemoteParticipant {
     pub user: Arc<User>,
     pub projects: Vec<proto::ParticipantProject>,
     pub location: ParticipantLocation,
+    pub tracks: HashMap<String, RemoteVideoTrack>,
+}
+
+#[derive(Clone)]
+pub struct RemoteVideoTrack {
+    pub(crate) frame: Option<CVImageBuffer>,
+    pub(crate) _live_kit_track: Arc<live_kit_client::RemoteVideoTrack>,
+    pub(crate) _maintain_frame: Arc<Task<()>>,
+}
+
+impl RemoteVideoTrack {
+    pub fn frame(&self) -> Option<&CVImageBuffer> {
+        self.frame.as_ref()
+    }
 }

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,7 +7,8 @@ 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::LocalVideoTrack;
+use live_kit_client::{LocalVideoTrack, RemoteVideoTrackChange};
+use postage::watch;
 use project::Project;
 use std::sync::Arc;
 use util::ResultExt;
@@ -27,7 +28,7 @@ pub enum Event {
 
 pub struct Room {
     id: u64,
-    live_kit_room: Option<Arc<live_kit_client::Room>>,
+    live_kit_room: Option<(Arc<live_kit_client::Room>, Task<()>)>,
     status: RoomStatus,
     local_participant: LocalParticipant,
     remote_participants: BTreeMap<PeerId, RemoteParticipant>,
@@ -75,17 +76,23 @@ impl Room {
         let live_kit_room = if let Some(connection_info) = live_kit_connection_info {
             let room = live_kit_client::Room::new();
             let mut tracks = room.remote_video_tracks();
-            cx.foreground()
-                .spawn(async move {
-                    while let Some(track) = tracks.next().await {
-                        dbg!("received track");
-                    }
-                })
-                .detach();
+            let maintain_room = cx.spawn_weak(|this, mut cx| async move {
+                while let Some(track_change) = tracks.next().await {
+                    let this = if let Some(this) = this.upgrade(&cx) {
+                        this
+                    } else {
+                        break;
+                    };
+
+                    this.update(&mut cx, |this, cx| {
+                        this.remote_video_track_changed(track_change, cx).log_err()
+                    });
+                }
+            });
             cx.foreground()
                 .spawn(room.connect(&connection_info.server_url, &connection_info.token))
                 .detach_and_log_err(cx);
-            Some(room)
+            Some((room, maintain_room))
         } else {
             None
         };
@@ -318,8 +325,20 @@ impl Room {
                                 projects: participant.projects,
                                 location: ParticipantLocation::from_proto(participant.location)
                                     .unwrap_or(ParticipantLocation::External),
+                                tracks: Default::default(),
                             },
                         );
+
+                        if let Some((room, _)) = this.live_kit_room.as_ref() {
+                            for track in
+                                room.video_tracks_for_remote_participant(peer_id.0.to_string())
+                            {
+                                this.remote_video_track_changed(
+                                    RemoteVideoTrackChange::Subscribed(track),
+                                    cx,
+                                );
+                            }
+                        }
                     }
 
                     this.remote_participants.retain(|_, participant| {
@@ -357,6 +376,74 @@ impl Room {
         Ok(())
     }
 
+    fn remote_video_track_changed(
+        &mut self,
+        change: RemoteVideoTrackChange,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        match change {
+            RemoteVideoTrackChange::Subscribed(track) => {
+                let peer_id = PeerId(track.publisher_id().parse()?);
+                let track_id = track.id().to_string();
+                let participant = self
+                    .remote_participants
+                    .get_mut(&peer_id)
+                    .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
+                let (mut tx, mut rx) = watch::channel();
+                track.add_renderer(move |frame| *tx.borrow_mut() = Some(frame));
+                participant.tracks.insert(
+                    track_id.clone(),
+                    RemoteVideoTrack {
+                        frame: None,
+                        _live_kit_track: track,
+                        _maintain_frame: Arc::new(cx.spawn_weak(|this, mut cx| async move {
+                            while let Some(frame) = rx.next().await {
+                                let this = if let Some(this) = this.upgrade(&cx) {
+                                    this
+                                } else {
+                                    break;
+                                };
+
+                                let done = this.update(&mut cx, |this, cx| {
+                                    // TODO: replace this with an emit.
+                                    cx.notify();
+                                    if let Some(track) =
+                                        this.remote_participants.get_mut(&peer_id).and_then(
+                                            |participant| participant.tracks.get_mut(&track_id),
+                                        )
+                                    {
+                                        track.frame = frame;
+                                        false
+                                    } else {
+                                        true
+                                    }
+                                });
+
+                                if done {
+                                    break;
+                                }
+                            }
+                        })),
+                    },
+                );
+            }
+            RemoteVideoTrackChange::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.notify();
+        Ok(())
+    }
+
     fn check_invariants(&self) {
         #[cfg(any(test, feature = "test-support"))]
         {
@@ -502,7 +589,7 @@ impl Room {
             return Task::ready(Err(anyhow!("room is offline")));
         }
 
-        let room = if let Some(room) = self.live_kit_room.as_ref() {
+        let room = if let Some((room, _)) = self.live_kit_room.as_ref() {
             room.clone()
         } else {
             return Task::ready(Err(anyhow!("not connected to LiveKit")));

crates/capture/Cargo.toml 🔗

@@ -1,29 +0,0 @@
-[package]
-name = "capture"
-version = "0.1.0"
-edition = "2021"
-description = "An example of screen capture"
-
-[dependencies]
-gpui = { path = "../gpui" }
-live_kit_client = { path = "../live_kit_client" }
-live_kit_server = { path = "../live_kit_server" }
-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"
-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"] }
-simplelog = "0.9"
-
-[build-dependencies]
-bindgen = "0.59.2"

crates/capture/build.rs 🔗

@@ -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");
-}

crates/capture/src/main.rs 🔗

@@ -1,150 +0,0 @@
-use futures::StreamExt;
-use gpui::{
-    actions,
-    elements::{Canvas, *},
-    keymap::Binding,
-    platform::current::Surface,
-    Menu, MenuItem, ViewContext,
-};
-use live_kit_client::{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_server::token::create(
-                &live_kit_key,
-                &live_kit_secret,
-                Some("test-participant-1"),
-                live_kit_server::token::VideoGrant {
-                    room: Some("test-room"),
-                    room_join: Some(true),
-                    can_publish: Some(true),
-                    can_subscribe: Some(true),
-                    ..Default::default()
-                },
-            )
-            .unwrap();
-            let room1 = Room::new();
-            room1.connect(&live_kit_url, &user1_token).await.unwrap();
-
-            let user2_token = live_kit_server::token::create(
-                &live_kit_key,
-                &live_kit_secret,
-                Some("test-participant-2"),
-                live_kit_server::token::VideoGrant {
-                    room: Some("test-room"),
-                    room_join: Some(true),
-                    can_publish: Some(true),
-                    can_subscribe: Some(true),
-                    ..Default::default()
-                },
-            )
-            .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 display_sources = live_kit_client::display_sources().await.unwrap();
-            let track = LocalVideoTrack::screen_share_for_display(display_sources.first().unwrap());
-            room1.publish_video_track(&track).await.unwrap();
-        })
-        .detach();
-    });
-}
-
-struct ScreenCaptureView {
-    image_buffer: Option<CVImageBuffer>,
-    _room: Arc<Room>,
-}
-
-impl gpui::Entity for ScreenCaptureView {
-    type Event = ();
-}
-
-impl ScreenCaptureView {
-    pub fn new(room: Arc<Room>, cx: &mut ViewContext<Self>) -> 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<Self>) -> 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();
-}

crates/collab/src/rpc.rs 🔗

@@ -504,7 +504,7 @@ impl Server {
 
             if let Some(room) = removed_connection.room {
                 self.room_updated(&room);
-                room_left = Some(self.room_left(&room, removed_connection.user_id));
+                room_left = Some(self.room_left(&room, connection_id));
             }
 
             contacts_to_update.insert(removed_connection.user_id);
@@ -613,7 +613,7 @@ impl Server {
                     .trace_err()
                 {
                     if let Some(token) = live_kit
-                        .room_token_for_user(&room.live_kit_room, &user_id.to_string())
+                        .room_token(&room.live_kit_room, &request.sender_id.to_string())
                         .trace_err()
                     {
                         Some(proto::LiveKitConnectionInfo {
@@ -658,7 +658,7 @@ impl Server {
             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_for_user(&room.live_kit_room, &user_id.to_string())
+                        .room_token(&room.live_kit_room, &request.sender_id.to_string())
                         .trace_err()
                     {
                         Some(proto::LiveKitConnectionInfo {
@@ -724,7 +724,7 @@ impl Server {
             }
 
             self.room_updated(&left_room.room);
-            room_left = self.room_left(&left_room.room, user_id);
+            room_left = self.room_left(&left_room.room, message.sender_id);
 
             for connection_id in left_room.canceled_call_connection_ids {
                 self.peer
@@ -883,13 +883,17 @@ impl Server {
         }
     }
 
-    fn room_left(&self, room: &proto::Room, user_id: UserId) -> impl Future<Output = Result<()>> {
+    fn room_left(
+        &self,
+        room: &proto::Room,
+        connection_id: ConnectionId,
+    ) -> impl Future<Output = Result<()>> {
         let client = self.app_state.live_kit_client.clone();
         let room_name = room.live_kit_room.clone();
         async move {
             if let Some(client) = client {
                 client
-                    .remove_participant(room_name, user_id.to_string())
+                    .remove_participant(room_name, connection_id.to_string())
                     .await?;
             }
 

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

@@ -4,16 +4,24 @@ import WebRTC
 
 class LKRoomDelegate: RoomDelegate {
     var data: UnsafeRawPointer
-    var onDidSubscribeToRemoteVideoTrack: @convention(c) (UnsafeRawPointer, UnsafeRawPointer) -> Void
+    var onDidSubscribeToRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void
+    var onDidUnsubscribeFromRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
     
-    init(data: UnsafeRawPointer, onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer) -> Void) {
+    init(data: UnsafeRawPointer, onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void, onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void) {
         self.data = data
         self.onDidSubscribeToRemoteVideoTrack = onDidSubscribeToRemoteVideoTrack
+        self.onDidUnsubscribeFromRemoteVideoTrack = onDidUnsubscribeFromRemoteVideoTrack
     }
 
     func room(_ room: Room, participant: RemoteParticipant, didSubscribe publication: RemoteTrackPublication, track: Track) {
         if track.kind == .video {
-            self.onDidSubscribeToRemoteVideoTrack(self.data, Unmanaged.passRetained(track).toOpaque())
+            self.onDidSubscribeToRemoteVideoTrack(self.data, participant.sid as CFString, track.id as CFString, Unmanaged.passRetained(track).toOpaque())
+        }
+    }
+    
+    func room(_ room: Room, participant: RemoteParticipant, didUnsubscribe publication: RemoteTrackPublication, track: Track) {
+        if track.kind == .video {
+            self.onDidUnsubscribeFromRemoteVideoTrack(self.data, participant.sid as CFString, track.id as CFString)
         }
     }
 }
@@ -53,8 +61,8 @@ public func LKRelease(ptr: UnsafeRawPointer)  {
 }
 
 @_cdecl("LKRoomDelegateCreate")
-public func LKRoomDelegateCreate(data: UnsafeRawPointer, onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer) -> Void) -> UnsafeMutableRawPointer {
-    let delegate = LKRoomDelegate(data: data, onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack)
+public func LKRoomDelegateCreate(data: UnsafeRawPointer, onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void, onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void) -> UnsafeMutableRawPointer {
+    let delegate = LKRoomDelegate(data: data, onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack, onDidUnsubscribeFromRemoteVideoTrack: onDidUnsubscribeFromRemoteVideoTrack)
     return Unmanaged.passRetained(delegate).toOpaque()
 }
 
@@ -86,6 +94,13 @@ public func LKRoomPublishVideoTrack(room: UnsafeRawPointer, track: UnsafeRawPoin
     }
 }
 
+@_cdecl("LKRoomVideoTracksForRemoteParticipant")
+public func LKRoomVideoTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
+    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
+    let tracks = room.remoteParticipants[participantId as Sid]?.videoTracks.compactMap { $0.track as? RemoteVideoTrack }
+    return tracks as CFArray?
+}
+
 @_cdecl("LKCreateScreenShareTrackForDisplay")
 public func LKCreateScreenShareTrackForDisplay(display: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer {
     let display = Unmanaged<MacOSDisplay>.fromOpaque(display).takeUnretainedValue()

crates/live_kit_client/src/live_kit_client.rs 🔗

@@ -22,8 +22,15 @@ extern "C" {
         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;
@@ -62,7 +69,7 @@ extern "C" {
 
 pub struct Room {
     native_room: *const c_void,
-    remote_video_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<Arc<RemoteVideoTrack>>>>,
+    remote_video_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteVideoTrackChange>>>,
     _delegate: RoomDelegate,
 }
 
@@ -103,7 +110,7 @@ impl Room {
         async { rx.await.unwrap().context("error publishing video track") }
     }
 
-    pub fn remote_video_tracks(&self) -> mpsc::UnboundedReceiver<Arc<RemoteVideoTrack>> {
+    pub fn remote_video_tracks(&self) -> mpsc::UnboundedReceiver<RemoteVideoTrackChange> {
         let (tx, rx) = mpsc::unbounded();
         self.remote_video_track_subscribers.lock().push(tx);
         rx
@@ -111,9 +118,20 @@ impl Room {
 
     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());
+        self.remote_video_track_subscribers.lock().retain(|tx| {
+            tx.unbounded_send(RemoteVideoTrackChange::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(RemoteVideoTrackChange::Unsubscribed {
+                publisher_id: publisher_id.clone(),
+                track_id: track_id.clone(),
+            })
+            .is_ok()
+        });
     }
 
     fn build_done_callback() -> (
@@ -157,6 +175,7 @@ impl RoomDelegate {
             LKRoomDelegateCreate(
                 weak_room as *mut c_void,
                 Self::on_did_subscribe_to_remote_video_track,
+                Self::on_did_unsubscribe_from_remote_video_track,
             )
         };
         Self {
@@ -165,14 +184,39 @@ impl RoomDelegate {
         }
     }
 
-    extern "C" fn on_did_subscribe_to_remote_video_track(room: *mut c_void, track: *const c_void) {
+    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 track = RemoteVideoTrack(track);
+        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 {
+            id: track_id,
+            native_track: track,
+            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 {
@@ -198,9 +242,22 @@ impl Drop for LocalVideoTrack {
     }
 }
 
-pub struct RemoteVideoTrack(*const c_void);
+#[derive(Debug)]
+pub struct RemoteVideoTrack {
+    id: String,
+    native_track: *const c_void,
+    publisher_id: String,
+}
 
 impl RemoteVideoTrack {
+    pub fn id(&self) -> &str {
+        &self.id
+    }
+
+    pub fn publisher_id(&self) -> &str {
+        &self.publisher_id
+    }
+
     pub fn add_renderer<F>(&self, callback: F)
     where
         F: 'static + FnMut(CVImageBuffer),
@@ -226,17 +283,25 @@ impl RemoteVideoTrack {
         unsafe {
             let renderer =
                 LKVideoRendererCreate(callback_data as *mut c_void, on_frame::<F>, on_drop::<F>);
-            LKVideoTrackAddRenderer(self.0, renderer);
+            LKVideoTrackAddRenderer(self.native_track, renderer);
         }
     }
 }
 
 impl Drop for RemoteVideoTrack {
     fn drop(&mut self) {
-        unsafe { LKRelease(self.0) }
+        unsafe { LKRelease(self.native_track) }
     }
 }
 
+pub enum RemoteVideoTrackChange {
+    Subscribed(Arc<RemoteVideoTrack>),
+    Unsubscribed {
+        publisher_id: String,
+        track_id: String,
+    },
+}
+
 pub struct MacOSDisplay(*const c_void);
 
 impl Drop for MacOSDisplay {

crates/live_kit_server/src/api.rs 🔗

@@ -78,7 +78,7 @@ impl Client {
         }
     }
 
-    pub fn room_token_for_user(&self, room: &str, identity: &str) -> Result<String> {
+    pub fn room_token(&self, room: &str, identity: &str) -> Result<String> {
         token::create(
             &self.key,
             &self.secret,

crates/workspace/src/pane_group.rs 🔗

@@ -201,21 +201,23 @@ impl Member {
                             .right()
                             .boxed(),
                         ),
-                        call::ParticipantLocation::External => Some(
-                            Label::new(
-                                format!(
-                                    "{} is viewing a window outside of Zed",
-                                    leader.user.github_login
-                                ),
-                                theme.workspace.external_location_message.text.clone(),
-                            )
-                            .contained()
-                            .with_style(theme.workspace.external_location_message.container)
-                            .aligned()
-                            .bottom()
-                            .right()
-                            .boxed(),
-                        ),
+                        call::ParticipantLocation::External => {
+                            let frame = leader
+                                .tracks
+                                .values()
+                                .next()
+                                .and_then(|track| track.frame())
+                                .cloned();
+                            return Canvas::new(move |bounds, _, cx| {
+                                if let Some(frame) = frame.clone() {
+                                    cx.scene.push_surface(gpui::mac::Surface {
+                                        bounds,
+                                        image_buffer: frame,
+                                    });
+                                }
+                            })
+                            .boxed();
+                        }
                     }
                 } else {
                     None