Detect when a track is unpublished due to reconnecting to livekit

Co-authored-by: Julia <julia@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-01-10 16:08:39 -08:00
parent 2d1eb0c56c
commit 75fdaeb56f
5 changed files with 195 additions and 17 deletions

View file

@ -1060,6 +1060,28 @@ impl Room {
participant_id: participant.peer_id, participant_id: participant.peer_id,
}); });
} }
RoomUpdate::LocalAudioTrackUnpublished { publication } => {
log::info!("unpublished audio track {}", publication.sid());
if let Some(room) = &mut self.live_kit {
room.microphone_track = LocalTrack::None;
}
}
RoomUpdate::LocalVideoTrackUnpublished { publication } => {
log::info!("unpublished video track {}", publication.sid());
if let Some(room) = &mut self.live_kit {
room.screen_track = LocalTrack::None;
}
}
RoomUpdate::LocalAudioTrackPublished { publication } => {
log::info!("published audio track {}", publication.sid());
}
RoomUpdate::LocalVideoTrackPublished { publication } => {
log::info!("published video track {}", publication.sid());
}
} }
cx.notify(); cx.notify();

View file

@ -12,6 +12,8 @@ class LKRoomDelegate: RoomDelegate {
var onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void var onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> 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
var onDidPublishOrUnpublishLocalAudioTrack: @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void
var onDidPublishOrUnpublishLocalVideoTrack: @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void
init( init(
data: UnsafeRawPointer, data: UnsafeRawPointer,
@ -21,7 +23,10 @@ class LKRoomDelegate: RoomDelegate {
onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void, onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void, onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void,
onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void, onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void) onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
onDidPublishOrUnpublishLocalAudioTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void,
onDidPublishOrUnpublishLocalVideoTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void
)
{ {
self.data = data self.data = data
self.onDidDisconnect = onDidDisconnect self.onDidDisconnect = onDidDisconnect
@ -31,6 +36,8 @@ class LKRoomDelegate: RoomDelegate {
self.onDidUnsubscribeFromRemoteVideoTrack = onDidUnsubscribeFromRemoteVideoTrack self.onDidUnsubscribeFromRemoteVideoTrack = onDidUnsubscribeFromRemoteVideoTrack
self.onMuteChangedFromRemoteAudioTrack = onMuteChangedFromRemoteAudioTrack self.onMuteChangedFromRemoteAudioTrack = onMuteChangedFromRemoteAudioTrack
self.onActiveSpeakersChanged = onActiveSpeakersChanged self.onActiveSpeakersChanged = onActiveSpeakersChanged
self.onDidPublishOrUnpublishLocalAudioTrack = onDidPublishOrUnpublishLocalAudioTrack
self.onDidPublishOrUnpublishLocalVideoTrack = onDidPublishOrUnpublishLocalVideoTrack
} }
func room(_ room: Room, didUpdate connectionState: ConnectionState, oldValue: ConnectionState) { func room(_ room: Room, didUpdate connectionState: ConnectionState, oldValue: ConnectionState) {
@ -65,6 +72,22 @@ class LKRoomDelegate: RoomDelegate {
self.onDidUnsubscribeFromRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString) self.onDidUnsubscribeFromRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString)
} }
} }
func room(_ room: Room, localParticipant: LocalParticipant, didPublish publication: LocalTrackPublication) {
if publication.kind == .video {
self.onDidPublishOrUnpublishLocalVideoTrack(self.data, Unmanaged.passUnretained(publication).toOpaque(), true)
} else if publication.kind == .audio {
self.onDidPublishOrUnpublishLocalAudioTrack(self.data, Unmanaged.passUnretained(publication).toOpaque(), true)
}
}
func room(_ room: Room, localParticipant: LocalParticipant, didUnpublish publication: LocalTrackPublication) {
if publication.kind == .video {
self.onDidPublishOrUnpublishLocalVideoTrack(self.data, Unmanaged.passUnretained(publication).toOpaque(), false)
} else if publication.kind == .audio {
self.onDidPublishOrUnpublishLocalAudioTrack(self.data, Unmanaged.passUnretained(publication).toOpaque(), false)
}
}
} }
class LKVideoRenderer: NSObject, VideoRenderer { class LKVideoRenderer: NSObject, VideoRenderer {
@ -109,7 +132,9 @@ public func LKRoomDelegateCreate(
onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void, onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
onActiveSpeakerChanged: @escaping @convention(c) (UnsafeRawPointer, CFArray) -> Void, onActiveSpeakerChanged: @escaping @convention(c) (UnsafeRawPointer, CFArray) -> Void,
onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void, onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
onDidPublishOrUnpublishLocalAudioTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void,
onDidPublishOrUnpublishLocalVideoTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer, Bool) -> Void
) -> UnsafeMutableRawPointer { ) -> UnsafeMutableRawPointer {
let delegate = LKRoomDelegate( let delegate = LKRoomDelegate(
data: data, data: data,
@ -119,7 +144,9 @@ public func LKRoomDelegateCreate(
onMuteChangedFromRemoteAudioTrack: onMuteChangedFromRemoteAudioTrack, onMuteChangedFromRemoteAudioTrack: onMuteChangedFromRemoteAudioTrack,
onActiveSpeakersChanged: onActiveSpeakerChanged, onActiveSpeakersChanged: onActiveSpeakerChanged,
onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack, onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack,
onDidUnsubscribeFromRemoteVideoTrack: onDidUnsubscribeFromRemoteVideoTrack onDidUnsubscribeFromRemoteVideoTrack: onDidUnsubscribeFromRemoteVideoTrack,
onDidPublishOrUnpublishLocalAudioTrack: onDidPublishOrUnpublishLocalAudioTrack,
onDidPublishOrUnpublishLocalVideoTrack: onDidPublishOrUnpublishLocalVideoTrack
) )
return Unmanaged.passRetained(delegate).toOpaque() return Unmanaged.passRetained(delegate).toOpaque()
} }
@ -292,6 +319,14 @@ public func LKLocalTrackPublicationSetMute(
} }
} }
@_cdecl("LKLocalTrackPublicationIsMuted")
public func LKLocalTrackPublicationIsMuted(
publication: UnsafeRawPointer
) -> Bool {
let publication = Unmanaged<LocalTrackPublication>.fromOpaque(publication).takeUnretainedValue()
return publication.muted
}
@_cdecl("LKRemoteTrackPublicationSetEnabled") @_cdecl("LKRemoteTrackPublicationSetEnabled")
public func LKRemoteTrackPublicationSetEnabled( public func LKRemoteTrackPublicationSetEnabled(
publication: UnsafeRawPointer, publication: UnsafeRawPointer,
@ -325,3 +360,12 @@ public func LKRemoteTrackPublicationGetSid(
return publication.sid as CFString return publication.sid as CFString
} }
@_cdecl("LKLocalTrackPublicationGetSid")
public func LKLocalTrackPublicationGetSid(
publication: UnsafeRawPointer
) -> CFString {
let publication = Unmanaged<LocalTrackPublication>.fromOpaque(publication).takeUnretainedValue()
return publication.sid as CFString
}

View file

@ -28,4 +28,8 @@ pub enum RoomUpdate {
SubscribedToRemoteAudioTrack(Arc<RemoteAudioTrack>, Arc<RemoteTrackPublication>), SubscribedToRemoteAudioTrack(Arc<RemoteAudioTrack>, Arc<RemoteTrackPublication>),
UnsubscribedFromRemoteVideoTrack { publisher_id: Sid, track_id: Sid }, UnsubscribedFromRemoteVideoTrack { publisher_id: Sid, track_id: Sid },
UnsubscribedFromRemoteAudioTrack { publisher_id: Sid, track_id: Sid }, UnsubscribedFromRemoteAudioTrack { publisher_id: Sid, track_id: Sid },
LocalAudioTrackPublished { publication: LocalTrackPublication },
LocalAudioTrackUnpublished { publication: LocalTrackPublication },
LocalVideoTrackPublished { publication: LocalTrackPublication },
LocalVideoTrackUnpublished { publication: LocalTrackPublication },
} }

View file

@ -77,6 +77,16 @@ extern "C" {
publisher_id: CFStringRef, publisher_id: CFStringRef,
track_id: CFStringRef, track_id: CFStringRef,
), ),
on_did_publish_or_unpublish_local_audio_track: extern "C" fn(
callback_data: *mut c_void,
publication: swift::LocalTrackPublication,
is_published: bool,
),
on_did_publish_or_unpublish_local_video_track: extern "C" fn(
callback_data: *mut c_void,
publication: swift::LocalTrackPublication,
is_published: bool,
),
) -> swift::RoomDelegate; ) -> swift::RoomDelegate;
fn LKRoomCreate(delegate: swift::RoomDelegate) -> swift::Room; fn LKRoomCreate(delegate: swift::RoomDelegate) -> swift::Room;
@ -152,7 +162,9 @@ extern "C" {
callback_data: *mut c_void, callback_data: *mut c_void,
); );
fn LKLocalTrackPublicationIsMuted(publication: swift::LocalTrackPublication) -> bool;
fn LKRemoteTrackPublicationIsMuted(publication: swift::RemoteTrackPublication) -> bool; fn LKRemoteTrackPublicationIsMuted(publication: swift::RemoteTrackPublication) -> bool;
fn LKLocalTrackPublicationGetSid(publication: swift::LocalTrackPublication) -> CFStringRef;
fn LKRemoteTrackPublicationGetSid(publication: swift::RemoteTrackPublication) -> CFStringRef; fn LKRemoteTrackPublicationGetSid(publication: swift::RemoteTrackPublication) -> CFStringRef;
} }
@ -511,6 +523,8 @@ impl RoomDelegate {
Self::on_active_speakers_changed, Self::on_active_speakers_changed,
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,
Self::on_did_publish_or_unpublish_local_audio_track,
Self::on_did_publish_or_unpublish_local_video_track,
) )
}; };
Self { Self {
@ -624,6 +638,46 @@ impl RoomDelegate {
} }
let _ = Weak::into_raw(room); let _ = Weak::into_raw(room);
} }
extern "C" fn on_did_publish_or_unpublish_local_audio_track(
room: *mut c_void,
publication: swift::LocalTrackPublication,
is_published: bool,
) {
let room = unsafe { Weak::from_raw(room as *mut Room) };
if let Some(room) = room.upgrade() {
let publication = LocalTrackPublication::new(publication);
let update = if is_published {
RoomUpdate::LocalAudioTrackPublished { publication }
} else {
RoomUpdate::LocalAudioTrackUnpublished { publication }
};
room.update_subscribers
.lock()
.retain(|tx| tx.unbounded_send(update.clone()).is_ok());
}
let _ = Weak::into_raw(room);
}
extern "C" fn on_did_publish_or_unpublish_local_video_track(
room: *mut c_void,
publication: swift::LocalTrackPublication,
is_published: bool,
) {
let room = unsafe { Weak::from_raw(room as *mut Room) };
if let Some(room) = room.upgrade() {
let publication = LocalTrackPublication::new(publication);
let update = if is_published {
RoomUpdate::LocalVideoTrackPublished { publication }
} else {
RoomUpdate::LocalVideoTrackUnpublished { publication }
};
room.update_subscribers
.lock()
.retain(|tx| tx.unbounded_send(update.clone()).is_ok());
}
let _ = Weak::into_raw(room);
}
} }
impl Drop for RoomDelegate { impl Drop for RoomDelegate {
@ -673,6 +727,10 @@ impl LocalTrackPublication {
Self(native_track_publication) Self(native_track_publication)
} }
pub fn sid(&self) -> String {
unsafe { CFString::wrap_under_get_rule(LKLocalTrackPublicationGetSid(self.0)).to_string() }
}
pub fn set_mute(&self, muted: bool) -> impl Future<Output = Result<()>> { pub fn set_mute(&self, muted: bool) -> impl Future<Output = Result<()>> {
let (tx, rx) = futures::channel::oneshot::channel(); let (tx, rx) = futures::channel::oneshot::channel();
@ -697,6 +755,19 @@ impl LocalTrackPublication {
async move { rx.await.unwrap() } async move { rx.await.unwrap() }
} }
pub fn is_muted(&self) -> bool {
unsafe { LKLocalTrackPublicationIsMuted(self.0) }
}
}
impl Clone for LocalTrackPublication {
fn clone(&self) -> Self {
unsafe {
CFRetain(self.0 .0);
}
Self(self.0)
}
} }
impl Drop for LocalTrackPublication { impl Drop for LocalTrackPublication {

View file

@ -8,7 +8,14 @@ use live_kit_server::{proto, token};
use media::core_video::CVImageBuffer; use media::core_video::CVImageBuffer;
use parking_lot::Mutex; use parking_lot::Mutex;
use postage::watch; use postage::watch;
use std::{future::Future, mem, sync::Arc}; use std::{
future::Future,
mem,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
};
static SERVERS: Mutex<BTreeMap<String, Arc<TestServer>>> = Mutex::new(BTreeMap::new()); static SERVERS: Mutex<BTreeMap<String, Arc<TestServer>>> = Mutex::new(BTreeMap::new());
@ -176,7 +183,11 @@ impl TestServer {
} }
} }
async fn publish_video_track(&self, token: String, local_track: LocalVideoTrack) -> Result<()> { async fn publish_video_track(
&self,
token: String,
local_track: LocalVideoTrack,
) -> Result<Sid> {
self.executor.simulate_random_delay().await; self.executor.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)?;
let identity = claims.sub.unwrap().to_string(); let identity = claims.sub.unwrap().to_string();
@ -198,8 +209,9 @@ impl TestServer {
return Err(anyhow!("user is not allowed to publish")); return Err(anyhow!("user is not allowed to publish"));
} }
let sid = nanoid::nanoid!(17);
let track = Arc::new(RemoteVideoTrack { let track = Arc::new(RemoteVideoTrack {
sid: nanoid::nanoid!(17), sid: sid.clone(),
publisher_id: identity.clone(), publisher_id: identity.clone(),
frames_rx: local_track.frames_rx.clone(), frames_rx: local_track.frames_rx.clone(),
}); });
@ -217,14 +229,14 @@ impl TestServer {
} }
} }
Ok(()) Ok(sid)
} }
async fn publish_audio_track( async fn publish_audio_track(
&self, &self,
token: String, token: String,
_local_track: &LocalAudioTrack, _local_track: &LocalAudioTrack,
) -> Result<()> { ) -> Result<Sid> {
self.executor.simulate_random_delay().await; self.executor.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)?;
let identity = claims.sub.unwrap().to_string(); let identity = claims.sub.unwrap().to_string();
@ -246,8 +258,9 @@ impl TestServer {
return Err(anyhow!("user is not allowed to publish")); return Err(anyhow!("user is not allowed to publish"));
} }
let sid = nanoid::nanoid!(17);
let track = Arc::new(RemoteAudioTrack { let track = Arc::new(RemoteAudioTrack {
sid: nanoid::nanoid!(17), sid: sid.clone(),
publisher_id: identity.clone(), publisher_id: identity.clone(),
}); });
@ -269,7 +282,7 @@ impl TestServer {
} }
} }
Ok(()) Ok(sid)
} }
fn video_tracks(&self, token: String) -> Result<Vec<Arc<RemoteVideoTrack>>> { fn video_tracks(&self, token: String) -> Result<Vec<Arc<RemoteVideoTrack>>> {
@ -425,10 +438,14 @@ impl Room {
let this = self.clone(); let this = self.clone();
let track = track.clone(); let track = track.clone();
async move { async move {
this.test_server() let sid = this
.test_server()
.publish_video_track(this.token(), track) .publish_video_track(this.token(), track)
.await?; .await?;
Ok(LocalTrackPublication) Ok(LocalTrackPublication {
muted: Default::default(),
sid,
})
} }
} }
pub fn publish_audio_track( pub fn publish_audio_track(
@ -438,10 +455,14 @@ impl Room {
let this = self.clone(); let this = self.clone();
let track = track.clone(); let track = track.clone();
async move { async move {
this.test_server() let sid = this
.test_server()
.publish_audio_track(this.token(), &track) .publish_audio_track(this.token(), &track)
.await?; .await?;
Ok(LocalTrackPublication) Ok(LocalTrackPublication {
muted: Default::default(),
sid,
})
} }
} }
@ -536,11 +557,27 @@ impl Drop for Room {
} }
} }
pub struct LocalTrackPublication; #[derive(Clone)]
pub struct LocalTrackPublication {
sid: String,
muted: Arc<AtomicBool>,
}
impl LocalTrackPublication { impl LocalTrackPublication {
pub fn set_mute(&self, _mute: bool) -> impl Future<Output = Result<()>> { pub fn set_mute(&self, mute: bool) -> impl Future<Output = Result<()>> {
async { Ok(()) } let muted = self.muted.clone();
async move {
muted.store(mute, SeqCst);
Ok(())
}
}
pub fn is_muted(&self) -> bool {
self.muted.load(SeqCst)
}
pub fn sid(&self) -> String {
self.sid.clone()
} }
} }