Mute mics by default (#2754)

Mikayla Maki created

This adds a setting to mute mics by default.

fixes https://github.com/zed-industries/community/issues/1769

Release notes:

- Fixed a bug with gutter spacing on files that end on a new significant
digit
- Added a setting for muting on join, and set it to true by default.

Change summary

Cargo.lock                                                                     |  4 
assets/settings/default.json                                                   |  5 
crates/call/Cargo.toml                                                         |  4 
crates/call/src/call.rs                                                        |  4 
crates/call/src/call_settings.rs                                               | 27 
crates/call/src/room.rs                                                        | 41 
crates/collab_ui/src/collab_titlebar_item.rs                                   |  8 
crates/collab_ui/src/collab_ui.rs                                              | 16 
crates/editor/src/element.rs                                                   |  2 
crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift | 46 
crates/live_kit_client/examples/test_app.rs                                    |  2 
crates/live_kit_client/src/prod.rs                                             | 32 
crates/live_kit_client/src/test.rs                                             | 17 
13 files changed, 157 insertions(+), 51 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1052,6 +1052,10 @@ dependencies = [
  "media",
  "postage",
  "project",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "serde_json",
  "settings",
  "util",
 ]

assets/settings/default.json 🔗

@@ -66,6 +66,11 @@
   // 3. Draw all invisible symbols:
   //   "all"
   "show_whitespaces": "selection",
+  // Settings related to calls in Zed
+  "calls": {
+    // Join calls with the microphone muted by default
+    "mute_on_join": true
+  },
   // Scrollbar related settings
   "scrollbar": {
     // When to show the scrollbar in the editor.

crates/call/Cargo.toml 🔗

@@ -36,6 +36,10 @@ anyhow.workspace = true
 async-broadcast = "0.4"
 futures.workspace = true
 postage.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+serde_derive.workspace = true
 
 [dev-dependencies]
 client = { path = "../client", features = ["test-support"] }

crates/call/src/call.rs 🔗

@@ -1,9 +1,11 @@
+pub mod call_settings;
 pub mod participant;
 pub mod room;
 
 use std::sync::Arc;
 
 use anyhow::{anyhow, Result};
+use call_settings::CallSettings;
 use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
 use collections::HashSet;
 use futures::{future::Shared, FutureExt};
@@ -19,6 +21,8 @@ pub use participant::ParticipantLocation;
 pub use room::Room;
 
 pub fn init(client: Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
+    settings::register::<CallSettings>(cx);
+
     let active_call = cx.add_model(|cx| ActiveCall::new(client, user_store, cx));
     cx.set_global(active_call);
 }

crates/call/src/call_settings.rs 🔗

@@ -0,0 +1,27 @@
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+use settings::Setting;
+
+#[derive(Deserialize, Debug)]
+pub struct CallSettings {
+    pub mute_on_join: bool,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct CallSettingsContent {
+    pub mute_on_join: Option<bool>,
+}
+
+impl Setting for CallSettings {
+    const KEY: Option<&'static str> = Some("calls");
+
+    type FileContent = CallSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}

crates/call/src/room.rs 🔗

@@ -1,4 +1,5 @@
 use crate::{
+    call_settings::CallSettings,
     participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack},
     IncomingCall,
 };
@@ -19,7 +20,7 @@ use live_kit_client::{
 };
 use postage::stream::Stream;
 use project::Project;
-use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration};
+use std::{future::Future, mem, panic::Location, pin::Pin, sync::Arc, time::Duration};
 use util::{post_inc, ResultExt, TryFutureExt};
 
 pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
@@ -153,8 +154,10 @@ impl Room {
             cx.spawn(|this, mut cx| async move {
                 connect.await?;
 
-                this.update(&mut cx, |this, cx| this.share_microphone(cx))
-                    .await?;
+                if !cx.read(|cx| settings::get::<CallSettings>(cx).mute_on_join) {
+                    this.update(&mut cx, |this, cx| this.share_microphone(cx))
+                        .await?;
+                }
 
                 anyhow::Ok(())
             })
@@ -656,7 +659,7 @@ impl Room {
                                     peer_id,
                                     projects: participant.projects,
                                     location,
-                                    muted: false,
+                                    muted: true,
                                     speaking: false,
                                     video_tracks: Default::default(),
                                     audio_tracks: Default::default(),
@@ -670,6 +673,10 @@ impl Room {
                                     live_kit.room.remote_video_tracks(&user.id.to_string());
                                 let audio_tracks =
                                     live_kit.room.remote_audio_tracks(&user.id.to_string());
+                                let publications = live_kit
+                                    .room
+                                    .remote_audio_track_publications(&user.id.to_string());
+
                                 for track in video_tracks {
                                     this.remote_video_track_updated(
                                         RemoteVideoTrackUpdate::Subscribed(track),
@@ -677,9 +684,15 @@ impl Room {
                                     )
                                     .log_err();
                                 }
-                                for track in audio_tracks {
+
+                                for (track, publication) in
+                                    audio_tracks.iter().zip(publications.iter())
+                                {
                                     this.remote_audio_track_updated(
-                                        RemoteAudioTrackUpdate::Subscribed(track),
+                                        RemoteAudioTrackUpdate::Subscribed(
+                                            track.clone(),
+                                            publication.clone(),
+                                        ),
                                         cx,
                                     )
                                     .log_err();
@@ -819,8 +832,8 @@ impl Room {
                 cx.notify();
             }
             RemoteAudioTrackUpdate::MuteChanged { track_id, muted } => {
+                let mut found = false;
                 for participant in &mut self.remote_participants.values_mut() {
-                    let mut found = false;
                     for track in participant.audio_tracks.values() {
                         if track.sid() == track_id {
                             found = true;
@@ -832,16 +845,20 @@ impl Room {
                         break;
                     }
                 }
+
                 cx.notify();
             }
-            RemoteAudioTrackUpdate::Subscribed(track) => {
+            RemoteAudioTrackUpdate::Subscribed(track, publication) => {
                 let user_id = track.publisher_id().parse()?;
                 let track_id = track.sid().to_string();
                 let participant = self
                     .remote_participants
                     .get_mut(&user_id)
                     .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
+
                 participant.audio_tracks.insert(track_id.clone(), track);
+                participant.muted = publication.is_muted();
+
                 cx.emit(Event::RemoteAudioTracksChanged {
                     participant_id: participant.peer_id,
                 });
@@ -1053,7 +1070,7 @@ impl Room {
         self.live_kit
             .as_ref()
             .and_then(|live_kit| match &live_kit.microphone_track {
-                LocalTrack::None => None,
+                LocalTrack::None => Some(true),
                 LocalTrack::Pending { muted, .. } => Some(*muted),
                 LocalTrack::Published { muted, .. } => Some(*muted),
             })
@@ -1070,7 +1087,9 @@ impl Room {
         self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
     }
 
+    #[track_caller]
     pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        dbg!(Location::caller());
         if self.status.is_offline() {
             return Task::ready(Err(anyhow!("room is offline")));
         } else if self.is_sharing_mic() {
@@ -1244,6 +1263,10 @@ impl Room {
     pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
         let should_mute = !self.is_muted();
         if let Some(live_kit) = self.live_kit.as_mut() {
+            if matches!(live_kit.microphone_track, LocalTrack::None) {
+                return Ok(self.share_microphone(cx));
+            }
+
             let (ret_task, old_muted) = live_kit.set_mute(should_mute, cx)?;
             live_kit.muted_by_user = should_mute;
 

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -652,10 +652,10 @@ impl CollabTitlebarItem {
         let is_muted = room.read(cx).is_muted();
         if is_muted {
             icon = "icons/radix/mic-mute.svg";
-            tooltip = "Unmute microphone\nRight click for options";
+            tooltip = "Unmute microphone";
         } else {
             icon = "icons/radix/mic.svg";
-            tooltip = "Mute microphone\nRight click for options";
+            tooltip = "Mute microphone";
         }
 
         let titlebar = &theme.titlebar;
@@ -705,10 +705,10 @@ impl CollabTitlebarItem {
         let is_deafened = room.read(cx).is_deafened().unwrap_or(false);
         if is_deafened {
             icon = "icons/radix/speaker-off.svg";
-            tooltip = "Unmute speakers\nRight click for options";
+            tooltip = "Unmute speakers";
         } else {
             icon = "icons/radix/speaker-loud.svg";
-            tooltip = "Mute speakers\nRight click for options";
+            tooltip = "Mute speakers";
         }
 
         let titlebar = &theme.titlebar;

crates/collab_ui/src/collab_ui.rs 🔗

@@ -18,13 +18,7 @@ use workspace::AppState;
 
 actions!(
     collab,
-    [
-        ToggleScreenSharing,
-        ToggleMute,
-        ToggleDeafen,
-        LeaveCall,
-        ShareMicrophone
-    ]
+    [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
 );
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
@@ -40,7 +34,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     cx.add_global_action(toggle_screen_sharing);
     cx.add_global_action(toggle_mute);
     cx.add_global_action(toggle_deafen);
-    cx.add_global_action(share_microphone);
 }
 
 pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
@@ -85,10 +78,3 @@ pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
             .log_err();
     }
 }
-
-pub fn share_microphone(_: &ShareMicrophone, cx: &mut AppContext) {
-    if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
-        room.update(cx, Room::share_microphone)
-            .detach_and_log_err(cx)
-    }
-}

crates/editor/src/element.rs 🔗

@@ -1311,7 +1311,7 @@ impl EditorElement {
     }
 
     fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext<Editor>) -> f32 {
-        let digit_count = (snapshot.max_buffer_row() as f32).log10().floor() as usize + 1;
+        let digit_count = (snapshot.max_buffer_row() as f32 + 1.).log10().floor() as usize + 1;
         let style = &self.style;
 
         cx.text_layout_cache()

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

@@ -6,7 +6,7 @@ import ScreenCaptureKit
 class LKRoomDelegate: RoomDelegate {
     var data: UnsafeRawPointer
     var onDidDisconnect: @convention(c) (UnsafeRawPointer) -> Void
-    var onDidSubscribeToRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void
+    var onDidSubscribeToRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void
     var onDidUnsubscribeFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
     var onMuteChangedFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void
     var onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void
@@ -16,7 +16,7 @@ class LKRoomDelegate: RoomDelegate {
     init(
         data: UnsafeRawPointer,
         onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void,
-        onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
+        onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void,
         onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
         onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
         onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void,
@@ -43,7 +43,7 @@ class LKRoomDelegate: RoomDelegate {
         if track.kind == .video {
             self.onDidSubscribeToRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque())
         } else if track.kind == .audio {
-            self.onDidSubscribeToRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque())
+            self.onDidSubscribeToRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque(), Unmanaged.passUnretained(publication).toOpaque())
         }
     }
 
@@ -52,12 +52,12 @@ class LKRoomDelegate: RoomDelegate {
             self.onMuteChangedFromRemoteAudioTrack(self.data, publication.sid as CFString, muted)
         }
     }
-    
+
     func room(_ room: Room, didUpdate speakers: [Participant]) {
         guard let speaker_ids = speakers.compactMap({ $0.identity as CFString }) as CFArray? else { return }
         self.onActiveSpeakersChanged(self.data, speaker_ids)
     }
-    
+
     func room(_ room: Room, participant: RemoteParticipant, didUnsubscribe publication: RemoteTrackPublication, track: Track) {
         if track.kind == .video {
             self.onDidUnsubscribeFromRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString)
@@ -104,7 +104,7 @@ class LKVideoRenderer: NSObject, VideoRenderer {
 public func LKRoomDelegateCreate(
     data: UnsafeRawPointer,
     onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void,
-    onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
+    onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void,
     onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
     onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
     onActiveSpeakerChanged: @escaping @convention(c) (UnsafeRawPointer, CFArray) -> Void,
@@ -180,39 +180,39 @@ public func LKRoomUnpublishTrack(room: UnsafeRawPointer, publication: UnsafeRawP
 @_cdecl("LKRoomAudioTracksForRemoteParticipant")
 public func LKRoomAudioTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
     let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
-    
+
     for (_, participant) in room.remoteParticipants {
         if participant.identity == participantId as String {
             return participant.audioTracks.compactMap { $0.track as? RemoteAudioTrack } as CFArray?
         }
     }
-    
+
     return nil;
 }
 
 @_cdecl("LKRoomAudioTrackPublicationsForRemoteParticipant")
 public func LKRoomAudioTrackPublicationsForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
     let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
-    
+
     for (_, participant) in room.remoteParticipants {
         if participant.identity == participantId as String {
             return participant.audioTracks.compactMap { $0 as? RemoteTrackPublication } as CFArray?
         }
     }
-    
+
     return nil;
 }
 
 @_cdecl("LKRoomVideoTracksForRemoteParticipant")
 public func LKRoomVideoTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
     let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
-    
+
     for (_, participant) in room.remoteParticipants {
         if participant.identity == participantId as String {
             return participant.videoTracks.compactMap { $0.track as? RemoteVideoTrack } as CFArray?
         }
     }
-    
+
     return nil;
 }
 
@@ -222,7 +222,7 @@ public func LKLocalAudioTrackCreateTrack() -> UnsafeMutableRawPointer {
       echoCancellation: true,
       noiseSuppression: true
     ))
-    
+
     return Unmanaged.passRetained(track).toOpaque()
 }
 
@@ -276,7 +276,7 @@ public func LKLocalTrackPublicationSetMute(
     callback_data: UnsafeRawPointer
 ) {
     let publication = Unmanaged<LocalTrackPublication>.fromOpaque(publication).takeUnretainedValue()
-    
+
     if muted {
         publication.mute().then {
             on_complete(callback_data, nil)
@@ -307,3 +307,21 @@ public func LKRemoteTrackPublicationSetEnabled(
         on_complete(callback_data, error.localizedDescription as CFString)
     }
 }
+
+@_cdecl("LKRemoteTrackPublicationIsMuted")
+public func LKRemoteTrackPublicationIsMuted(
+    publication: UnsafeRawPointer
+) -> Bool {
+    let publication = Unmanaged<RemoteTrackPublication>.fromOpaque(publication).takeUnretainedValue()
+
+    return publication.muted
+}
+
+@_cdecl("LKRemoteTrackPublicationGetSid")
+public func LKRemoteTrackPublicationGetSid(
+    publication: UnsafeRawPointer
+) -> CFString {
+    let publication = Unmanaged<RemoteTrackPublication>.fromOpaque(publication).takeUnretainedValue()
+
+    return publication.sid as CFString
+}

crates/live_kit_client/examples/test_app.rs 🔗

@@ -63,7 +63,7 @@ fn main() {
             let audio_track = LocalAudioTrack::create();
             let audio_track_publication = room_a.publish_audio_track(&audio_track).await.unwrap();
 
-            if let RemoteAudioTrackUpdate::Subscribed(track) =
+            if let RemoteAudioTrackUpdate::Subscribed(track, _) =
                 audio_track_updates.next().await.unwrap()
             {
                 let remote_tracks = room_b.remote_audio_tracks("test-participant-1");

crates/live_kit_client/src/prod.rs 🔗

@@ -26,6 +26,7 @@ extern "C" {
             publisher_id: CFStringRef,
             track_id: CFStringRef,
             remote_track: *const c_void,
+            remote_publication: *const c_void,
         ),
         on_did_unsubscribe_from_remote_audio_track: extern "C" fn(
             callback_data: *mut c_void,
@@ -125,6 +126,9 @@ extern "C" {
         on_complete: extern "C" fn(callback_data: *mut c_void, error: CFStringRef),
         callback_data: *mut c_void,
     );
+
+    fn LKRemoteTrackPublicationIsMuted(publication: *const c_void) -> bool;
+    fn LKRemoteTrackPublicationGetSid(publication: *const c_void) -> CFStringRef;
 }
 
 pub type Sid = String;
@@ -372,11 +376,19 @@ impl Room {
         rx
     }
 
-    fn did_subscribe_to_remote_audio_track(&self, track: RemoteAudioTrack) {
+    fn did_subscribe_to_remote_audio_track(
+        &self,
+        track: RemoteAudioTrack,
+        publication: RemoteTrackPublication,
+    ) {
         let track = Arc::new(track);
+        let publication = Arc::new(publication);
         self.remote_audio_track_subscribers.lock().retain(|tx| {
-            tx.unbounded_send(RemoteAudioTrackUpdate::Subscribed(track.clone()))
-                .is_ok()
+            tx.unbounded_send(RemoteAudioTrackUpdate::Subscribed(
+                track.clone(),
+                publication.clone(),
+            ))
+            .is_ok()
         });
     }
 
@@ -501,13 +513,15 @@ impl RoomDelegate {
         publisher_id: CFStringRef,
         track_id: CFStringRef,
         track: *const c_void,
+        publication: *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 = RemoteAudioTrack::new(track, track_id, publisher_id);
+        let publication = RemoteTrackPublication::new(publication);
         if let Some(room) = room.upgrade() {
-            room.did_subscribe_to_remote_audio_track(track);
+            room.did_subscribe_to_remote_audio_track(track, publication);
         }
         let _ = Weak::into_raw(room);
     }
@@ -682,6 +696,14 @@ impl RemoteTrackPublication {
         Self(native_track_publication)
     }
 
+    pub fn sid(&self) -> String {
+        unsafe { CFString::wrap_under_get_rule(LKRemoteTrackPublicationGetSid(self.0)).to_string() }
+    }
+
+    pub fn is_muted(&self) -> bool {
+        unsafe { LKRemoteTrackPublicationIsMuted(self.0) }
+    }
+
     pub fn set_enabled(&self, enabled: bool) -> impl Future<Output = Result<()>> {
         let (tx, rx) = futures::channel::oneshot::channel();
 
@@ -832,7 +854,7 @@ pub enum RemoteVideoTrackUpdate {
 pub enum RemoteAudioTrackUpdate {
     ActiveSpeakersChanged { speakers: Vec<Sid> },
     MuteChanged { track_id: Sid, muted: bool },
-    Subscribed(Arc<RemoteAudioTrack>),
+    Subscribed(Arc<RemoteAudioTrack>, Arc<RemoteTrackPublication>),
     Unsubscribed { publisher_id: Sid, track_id: Sid },
 }
 

crates/live_kit_client/src/test.rs 🔗

@@ -216,6 +216,8 @@ impl TestServer {
             publisher_id: identity.clone(),
         });
 
+        let publication = Arc::new(RemoteTrackPublication);
+
         room.audio_tracks.push(track.clone());
 
         for (id, client_room) in &room.client_rooms {
@@ -225,7 +227,10 @@ impl TestServer {
                     .lock()
                     .audio_track_updates
                     .0
-                    .try_broadcast(RemoteAudioTrackUpdate::Subscribed(track.clone()))
+                    .try_broadcast(RemoteAudioTrackUpdate::Subscribed(
+                        track.clone(),
+                        publication.clone(),
+                    ))
                     .unwrap();
             }
         }
@@ -501,6 +506,14 @@ impl RemoteTrackPublication {
     pub fn set_enabled(&self, _enabled: bool) -> impl Future<Output = Result<()>> {
         async { Ok(()) }
     }
+
+    pub fn is_muted(&self) -> bool {
+        false
+    }
+
+    pub fn sid(&self) -> String {
+        "".to_string()
+    }
 }
 
 #[derive(Clone)]
@@ -579,7 +592,7 @@ pub enum RemoteVideoTrackUpdate {
 pub enum RemoteAudioTrackUpdate {
     ActiveSpeakersChanged { speakers: Vec<Sid> },
     MuteChanged { track_id: Sid, muted: bool },
-    Subscribed(Arc<RemoteAudioTrack>),
+    Subscribed(Arc<RemoteAudioTrack>, Arc<RemoteTrackPublication>),
     Unsubscribed { publisher_id: Sid, track_id: Sid },
 }