Leave Zed room when LiveKit room disconnects

This commit is contained in:
Antonio Scandurra 2022-10-21 14:21:45 +02:00
parent 78969d0938
commit 1bbb7dd126
7 changed files with 200 additions and 38 deletions

View file

@ -8,6 +8,7 @@ use collections::{BTreeMap, HashSet};
use futures::StreamExt; use futures::StreamExt;
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate}; use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate};
use postage::stream::Stream;
use project::Project; use project::Project;
use std::{mem, os::unix::prelude::OsStrExt, sync::Arc}; use std::{mem, os::unix::prelude::OsStrExt, sync::Arc};
use util::{post_inc, ResultExt}; use util::{post_inc, ResultExt};
@ -80,8 +81,26 @@ impl Room {
let live_kit_room = if let Some(connection_info) = live_kit_connection_info { let live_kit_room = if let Some(connection_info) = live_kit_connection_info {
let room = live_kit_client::Room::new(); let room = live_kit_client::Room::new();
let mut track_changes = room.remote_video_track_updates(); 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 { 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 { while let Some(track_change) = track_changes.next().await {
let this = if let Some(this) = this.upgrade(&cx) { let this = if let Some(this) = this.upgrade(&cx) {
this this
@ -94,14 +113,17 @@ impl Room {
}); });
} }
}); });
cx.foreground() cx.foreground()
.spawn(room.connect(&connection_info.server_url, &connection_info.token)) .spawn(room.connect(&connection_info.server_url, &connection_info.token))
.detach_and_log_err(cx); .detach_and_log_err(cx);
Some(LiveKitRoom { Some(LiveKitRoom {
room, room,
screen_track: ScreenTrack::None, screen_track: ScreenTrack::None,
next_publish_id: 0, next_publish_id: 0,
_maintain_room, _maintain_room,
_maintain_tracks,
}) })
} else { } else {
None None
@ -725,6 +747,7 @@ struct LiveKitRoom {
screen_track: ScreenTrack, screen_track: ScreenTrack,
next_publish_id: usize, next_publish_id: usize,
_maintain_room: Task<()>, _maintain_room: Task<()>,
_maintain_tracks: Task<()>,
} }
pub enum ScreenTrack { pub enum ScreenTrack {

View file

@ -253,12 +253,13 @@ async fn test_basic_calls(
} }
); );
// User B leaves the room. // User B gets disconnected from the LiveKit server, which causes them
active_call_b.update(cx_b, |call, cx| { // to automatically leave the room.
call.hang_up(cx).unwrap(); server
assert!(call.room().is_none()); .test_live_kit_server
}); .disconnect_client(client_b.peer_id().unwrap().to_string())
deterministic.run_until_parked(); .await;
active_call_b.update(cx_b, |call, _| assert!(call.room().is_none()));
assert_eq!( assert_eq!(
room_participants(&room_a, cx_a), room_participants(&room_a, cx_a),
RoomParticipants { RoomParticipants {
@ -452,6 +453,63 @@ async fn test_leaving_room_on_disconnection(
pending: Default::default() pending: Default::default()
} }
); );
// Call user B again from client A.
active_call_a
.update(cx_a, |call, cx| {
call.invite(client_b.user_id().unwrap(), None, cx)
})
.await
.unwrap();
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
// User B receives the call and joins the room.
let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
incoming_call_b.next().await.unwrap().unwrap();
active_call_b
.update(cx_b, |call, cx| call.accept_incoming(cx))
.await
.unwrap();
let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
deterministic.run_until_parked();
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
remote: vec!["user_b".to_string()],
pending: Default::default()
}
);
assert_eq!(
room_participants(&room_b, cx_b),
RoomParticipants {
remote: vec!["user_a".to_string()],
pending: Default::default()
}
);
// User B gets disconnected from the LiveKit server, which causes it
// to automatically leave the room.
server
.test_live_kit_server
.disconnect_client(client_b.peer_id().unwrap().to_string())
.await;
deterministic.run_until_parked();
active_call_a.update(cx_a, |call, _| assert!(call.room().is_none()));
active_call_b.update(cx_b, |call, _| assert!(call.room().is_none()));
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
remote: Default::default(),
pending: Default::default()
}
);
assert_eq!(
room_participants(&room_b, cx_b),
RoomParticipants {
remote: Default::default(),
pending: Default::default()
}
);
} }
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]

View file

@ -34,6 +34,7 @@ core-graphics = "0.22.3"
futures = "0.3" futures = "0.3"
log = { version = "0.4.16", features = ["kv_unstable_serde"] } log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1" parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
async-trait = { version = "0.1", optional = true } async-trait = { version = "0.1", optional = true }
lazy_static = { version = "1.4", optional = true } lazy_static = { version = "1.4", optional = true }
@ -60,7 +61,6 @@ jwt = "0.16"
lazy_static = "1.4" lazy_static = "1.4"
objc = "0.2" objc = "0.2"
parking_lot = "0.11.1" parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
serde = { version = "1.0", features = ["derive", "rc"] } serde = { version = "1.0", features = ["derive", "rc"] }
sha2 = "0.10" sha2 = "0.10"
simplelog = "0.9" simplelog = "0.9"

View file

@ -5,15 +5,28 @@ import ScreenCaptureKit
class LKRoomDelegate: RoomDelegate { class LKRoomDelegate: RoomDelegate {
var data: UnsafeRawPointer var data: UnsafeRawPointer
var onDidDisconnect: @convention(c) (UnsafeRawPointer) -> Void
var onDidSubscribeToRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void var onDidSubscribeToRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void
var onDidUnsubscribeFromRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void var onDidUnsubscribeFromRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
init(data: UnsafeRawPointer, onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void, onDidUnsubscribeFromRemoteVideoTrack: @escaping @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.data = data
self.onDidDisconnect = onDidDisconnect
self.onDidSubscribeToRemoteVideoTrack = onDidSubscribeToRemoteVideoTrack self.onDidSubscribeToRemoteVideoTrack = onDidSubscribeToRemoteVideoTrack
self.onDidUnsubscribeFromRemoteVideoTrack = onDidUnsubscribeFromRemoteVideoTrack 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) { func room(_ room: Room, participant: RemoteParticipant, didSubscribe publication: RemoteTrackPublication, track: Track) {
if track.kind == .video { if track.kind == .video {
self.onDidSubscribeToRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque()) self.onDidSubscribeToRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque())
@ -62,8 +75,18 @@ class LKVideoRenderer: NSObject, VideoRenderer {
} }
@_cdecl("LKRoomDelegateCreate") @_cdecl("LKRoomDelegateCreate")
public func LKRoomDelegateCreate(data: UnsafeRawPointer, onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void, onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void) -> UnsafeMutableRawPointer { public func LKRoomDelegateCreate(
let delegate = LKRoomDelegate(data: data, onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack, onDidUnsubscribeFromRemoteVideoTrack: onDidUnsubscribeFromRemoteVideoTrack) 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() return Unmanaged.passRetained(delegate).toOpaque()
} }

View file

@ -11,6 +11,7 @@ use futures::{
pub use media::core_video::CVImageBuffer; pub use media::core_video::CVImageBuffer;
use media::core_video::CVImageBufferRef; use media::core_video::CVImageBufferRef;
use parking_lot::Mutex; use parking_lot::Mutex;
use postage::watch;
use std::{ use std::{
ffi::c_void, ffi::c_void,
sync::{Arc, Weak}, sync::{Arc, Weak},
@ -19,6 +20,7 @@ use std::{
extern "C" { extern "C" {
fn LKRoomDelegateCreate( fn LKRoomDelegateCreate(
callback_data: *mut c_void, 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( on_did_subscribe_to_remote_video_track: extern "C" fn(
callback_data: *mut c_void, callback_data: *mut c_void,
publisher_id: CFStringRef, publisher_id: CFStringRef,
@ -75,8 +77,18 @@ extern "C" {
pub type Sid = String; pub type Sid = String;
#[derive(Clone, Eq, PartialEq)]
pub enum ConnectionState {
Disconnected,
Connected { url: String, token: String },
}
pub struct Room { pub struct Room {
native_room: *const c_void, native_room: *const c_void,
connection: Mutex<(
watch::Sender<ConnectionState>,
watch::Receiver<ConnectionState>,
)>,
remote_video_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteVideoTrackUpdate>>>, remote_video_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<RemoteVideoTrackUpdate>>>,
_delegate: RoomDelegate, _delegate: RoomDelegate,
} }
@ -87,13 +99,18 @@ impl Room {
let delegate = RoomDelegate::new(weak_room.clone()); let delegate = RoomDelegate::new(weak_room.clone());
Self { Self {
native_room: unsafe { LKRoomCreate(delegate.native_delegate) }, native_room: unsafe { LKRoomCreate(delegate.native_delegate) },
connection: Mutex::new(watch::channel_with(ConnectionState::Disconnected)),
remote_video_track_subscribers: Default::default(), remote_video_track_subscribers: Default::default(),
_delegate: delegate, _delegate: delegate,
} }
}) })
} }
pub fn connect(&self, url: &str, token: &str) -> impl Future<Output = Result<()>> { pub fn status(&self) -> watch::Receiver<ConnectionState> {
self.connection.lock().1.clone()
}
pub fn connect(self: &Arc<Self>, url: &str, token: &str) -> impl Future<Output = Result<()>> {
let url = CFString::new(url); let url = CFString::new(url);
let token = CFString::new(token); let token = CFString::new(token);
let (did_connect, tx, rx) = Self::build_done_callback(); let (did_connect, tx, rx) = Self::build_done_callback();
@ -107,7 +124,23 @@ impl Room {
) )
} }
async { rx.await.unwrap().context("error connecting to room") } 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<Self>) -> impl Future<Output = Result<Vec<MacOSDisplay>>> { pub fn display_sources(self: &Arc<Self>) -> impl Future<Output = Result<Vec<MacOSDisplay>>> {
@ -265,6 +298,7 @@ impl RoomDelegate {
let native_delegate = unsafe { let native_delegate = unsafe {
LKRoomDelegateCreate( LKRoomDelegateCreate(
weak_room as *mut c_void, weak_room as *mut c_void,
Self::on_did_disconnect,
Self::on_did_subscribe_to_remote_video_track, Self::on_did_subscribe_to_remote_video_track,
Self::on_did_unsubscribe_from_remote_video_track, Self::on_did_unsubscribe_from_remote_video_track,
) )
@ -275,6 +309,14 @@ impl RoomDelegate {
} }
} }
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( extern "C" fn on_did_subscribe_to_remote_video_track(
room: *mut c_void, room: *mut c_void,
publisher_id: CFStringRef, publisher_id: CFStringRef,

View file

@ -7,7 +7,8 @@ use lazy_static::lazy_static;
use live_kit_server::token; use live_kit_server::token;
use media::core_video::CVImageBuffer; use media::core_video::CVImageBuffer;
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{future::Future, sync::Arc}; use postage::watch;
use std::{future::Future, mem, sync::Arc};
lazy_static! { lazy_static! {
static ref SERVERS: Mutex<HashMap<String, Arc<TestServer>>> = Default::default(); static ref SERVERS: Mutex<HashMap<String, Arc<TestServer>>> = Default::default();
@ -145,6 +146,16 @@ impl TestServer {
Ok(()) 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<()> { async fn publish_video_track(&self, token: String, local_track: LocalVideoTrack) -> Result<()> {
self.background.simulate_random_delay().await; self.background.simulate_random_delay().await;
let claims = live_kit_server::token::validate(&token, &self.secret_key)?; let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
@ -227,7 +238,10 @@ impl live_kit_server::api::Client for TestApiClient {
pub type Sid = String; pub type Sid = String;
struct RoomState { struct RoomState {
connection: Option<ConnectionState>, connection: (
watch::Sender<ConnectionState>,
watch::Receiver<ConnectionState>,
),
display_sources: Vec<MacOSDisplay>, display_sources: Vec<MacOSDisplay>,
video_track_updates: ( video_track_updates: (
async_broadcast::Sender<RemoteVideoTrackUpdate>, async_broadcast::Sender<RemoteVideoTrackUpdate>,
@ -235,9 +249,10 @@ struct RoomState {
), ),
} }
struct ConnectionState { #[derive(Clone, Eq, PartialEq)]
url: String, pub enum ConnectionState {
token: String, Disconnected,
Connected { url: String, token: String },
} }
pub struct Room(Mutex<RoomState>); pub struct Room(Mutex<RoomState>);
@ -245,12 +260,16 @@ pub struct Room(Mutex<RoomState>);
impl Room { impl Room {
pub fn new() -> Arc<Self> { pub fn new() -> Arc<Self> {
Arc::new(Self(Mutex::new(RoomState { Arc::new(Self(Mutex::new(RoomState {
connection: None, connection: watch::channel_with(ConnectionState::Disconnected),
display_sources: Default::default(), display_sources: Default::default(),
video_track_updates: async_broadcast::broadcast(128), video_track_updates: async_broadcast::broadcast(128),
}))) })))
} }
pub fn status(&self) -> watch::Receiver<ConnectionState> {
self.0.lock().connection.1.clone()
}
pub fn connect(self: &Arc<Self>, url: &str, token: &str) -> impl Future<Output = Result<()>> { pub fn connect(self: &Arc<Self>, url: &str, token: &str) -> impl Future<Output = Result<()>> {
let this = self.clone(); let this = self.clone();
let url = url.to_string(); let url = url.to_string();
@ -258,7 +277,7 @@ impl Room {
async move { async move {
let server = TestServer::get(&url)?; let server = TestServer::get(&url)?;
server.join_room(token.clone(), this.clone()).await?; server.join_room(token.clone(), this.clone()).await?;
this.0.lock().connection = Some(ConnectionState { url, token }); *this.0.lock().connection.0.borrow_mut() = ConnectionState::Connected { url, token };
Ok(()) Ok(())
} }
} }
@ -301,32 +320,30 @@ impl Room {
} }
fn test_server(&self) -> Arc<TestServer> { fn test_server(&self) -> Arc<TestServer> {
let this = self.0.lock(); match self.0.lock().connection.1.borrow().clone() {
let connection = this ConnectionState::Disconnected => panic!("must be connected to call this method"),
.connection ConnectionState::Connected { url, .. } => TestServer::get(&url).unwrap(),
.as_ref() }
.expect("must be connected to call this method");
TestServer::get(&connection.url).unwrap()
} }
fn token(&self) -> String { fn token(&self) -> String {
self.0 match self.0.lock().connection.1.borrow().clone() {
.lock() ConnectionState::Disconnected => panic!("must be connected to call this method"),
.connection ConnectionState::Connected { token, .. } => token,
.as_ref() }
.expect("must be connected to call this method")
.token
.clone()
} }
} }
impl Drop for Room { impl Drop for Room {
fn drop(&mut self) { fn drop(&mut self) {
if let Some(connection) = self.0.lock().connection.take() { if let ConnectionState::Connected { token, .. } = mem::replace(
if let Ok(server) = TestServer::get(&connection.token) { &mut *self.0.lock().connection.0.borrow_mut(),
ConnectionState::Disconnected,
) {
if let Ok(server) = TestServer::get(&token) {
let background = server.background.clone(); let background = server.background.clone();
background background
.spawn(async move { server.leave_room(connection.token).await.unwrap() }) .spawn(async move { server.leave_room(token).await.unwrap() })
.detach(); .detach();
} }
} }

View file

@ -86,7 +86,7 @@ impl Client for LiveKitClient {
} }
async fn create_room(&self, name: String) -> Result<()> { async fn create_room(&self, name: String) -> Result<()> {
let x: proto::Room = self let _: proto::Room = self
.request( .request(
"twirp/livekit.RoomService/CreateRoom", "twirp/livekit.RoomService/CreateRoom",
token::VideoGrant { token::VideoGrant {
@ -99,7 +99,6 @@ impl Client for LiveKitClient {
}, },
) )
.await?; .await?;
dbg!(x);
Ok(()) Ok(())
} }