
Adds component NotificationFrame. It implements a subset of MessageNotification as a Component and refactors MessageNotification to use NotificationFrame. Having some notification UI Component is nice as it allows us to easily build new types of notifications. Uses the new NotificationFrame component for CaptureAudioNotification. Adds a CaptureAudio action in the dev namespace (not meant for end-users). It records 10 seconds of audio and saves that to a wav file. Release Notes: - N/A --------- Co-authored-by: Mikayla <mikayla@zed.dev>
455 lines
14 KiB
Rust
455 lines
14 KiB
Rust
use std::sync::Arc;
|
|
|
|
use anyhow::{Context as _, Result};
|
|
use collections::HashMap;
|
|
use futures::{SinkExt, channel::mpsc};
|
|
use gpui::{App, AsyncApp, ScreenCaptureSource, ScreenCaptureStream, Task};
|
|
use gpui_tokio::Tokio;
|
|
use playback::capture_local_video_track;
|
|
|
|
mod playback;
|
|
#[cfg(feature = "record-microphone")]
|
|
mod record;
|
|
|
|
use crate::{LocalTrack, Participant, RemoteTrack, RoomEvent, TrackPublication};
|
|
pub use playback::AudioStream;
|
|
pub(crate) use playback::{RemoteVideoFrame, play_remote_video_track};
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct RemoteVideoTrack(livekit::track::RemoteVideoTrack);
|
|
#[derive(Clone, Debug)]
|
|
pub struct RemoteAudioTrack(livekit::track::RemoteAudioTrack);
|
|
#[derive(Clone, Debug)]
|
|
pub struct RemoteTrackPublication(livekit::publication::RemoteTrackPublication);
|
|
#[derive(Clone, Debug)]
|
|
pub struct RemoteParticipant(livekit::participant::RemoteParticipant);
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct LocalVideoTrack(livekit::track::LocalVideoTrack);
|
|
#[derive(Clone, Debug)]
|
|
pub struct LocalAudioTrack(livekit::track::LocalAudioTrack);
|
|
#[derive(Clone, Debug)]
|
|
pub struct LocalTrackPublication(livekit::publication::LocalTrackPublication);
|
|
#[derive(Clone, Debug)]
|
|
pub struct LocalParticipant(livekit::participant::LocalParticipant);
|
|
|
|
pub struct Room {
|
|
room: livekit::Room,
|
|
_task: Task<()>,
|
|
playback: playback::AudioStack,
|
|
}
|
|
|
|
pub type TrackSid = livekit::id::TrackSid;
|
|
pub type ConnectionState = livekit::ConnectionState;
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
|
pub struct ParticipantIdentity(pub String);
|
|
|
|
impl Room {
|
|
pub async fn connect(
|
|
url: String,
|
|
token: String,
|
|
cx: &mut AsyncApp,
|
|
) -> Result<(Self, mpsc::UnboundedReceiver<RoomEvent>)> {
|
|
let connector =
|
|
tokio_tungstenite::Connector::Rustls(Arc::new(http_client_tls::tls_config()));
|
|
let mut config = livekit::RoomOptions::default();
|
|
config.connector = Some(connector);
|
|
let (room, mut events) = Tokio::spawn(cx, async move {
|
|
livekit::Room::connect(&url, &token, config).await
|
|
})?
|
|
.await??;
|
|
|
|
let (mut tx, rx) = mpsc::unbounded();
|
|
let task = cx.background_executor().spawn(async move {
|
|
while let Some(event) = events.recv().await {
|
|
if let Some(event) = room_event_from_livekit(event) {
|
|
tx.send(event).await.ok();
|
|
}
|
|
}
|
|
});
|
|
|
|
Ok((
|
|
Self {
|
|
room,
|
|
_task: task,
|
|
playback: playback::AudioStack::new(cx.background_executor().clone()),
|
|
},
|
|
rx,
|
|
))
|
|
}
|
|
|
|
pub fn local_participant(&self) -> LocalParticipant {
|
|
LocalParticipant(self.room.local_participant())
|
|
}
|
|
|
|
pub fn remote_participants(&self) -> HashMap<ParticipantIdentity, RemoteParticipant> {
|
|
self.room
|
|
.remote_participants()
|
|
.into_iter()
|
|
.map(|(k, v)| (ParticipantIdentity(k.0), RemoteParticipant(v)))
|
|
.collect()
|
|
}
|
|
|
|
pub fn connection_state(&self) -> ConnectionState {
|
|
self.room.connection_state()
|
|
}
|
|
|
|
pub async fn publish_local_microphone_track(
|
|
&self,
|
|
cx: &mut AsyncApp,
|
|
) -> Result<(LocalTrackPublication, playback::AudioStream)> {
|
|
let (track, stream) = self.playback.capture_local_microphone_track()?;
|
|
let publication = self
|
|
.local_participant()
|
|
.publish_track(
|
|
livekit::track::LocalTrack::Audio(track.0),
|
|
livekit::options::TrackPublishOptions {
|
|
source: livekit::track::TrackSource::Microphone,
|
|
..Default::default()
|
|
},
|
|
cx,
|
|
)
|
|
.await?;
|
|
|
|
Ok((publication, stream))
|
|
}
|
|
|
|
pub async fn unpublish_local_track(
|
|
&self,
|
|
sid: TrackSid,
|
|
cx: &mut AsyncApp,
|
|
) -> Result<LocalTrackPublication> {
|
|
self.local_participant().unpublish_track(sid, cx).await
|
|
}
|
|
|
|
pub fn play_remote_audio_track(
|
|
&self,
|
|
track: &RemoteAudioTrack,
|
|
_cx: &App,
|
|
) -> Result<playback::AudioStream> {
|
|
Ok(self.playback.play_remote_audio_track(&track.0))
|
|
}
|
|
}
|
|
|
|
impl LocalParticipant {
|
|
pub async fn publish_screenshare_track(
|
|
&self,
|
|
source: &dyn ScreenCaptureSource,
|
|
cx: &mut AsyncApp,
|
|
) -> Result<(LocalTrackPublication, Box<dyn ScreenCaptureStream>)> {
|
|
let (track, stream) = capture_local_video_track(source, cx).await?;
|
|
let options = livekit::options::TrackPublishOptions {
|
|
source: livekit::track::TrackSource::Screenshare,
|
|
video_codec: livekit::options::VideoCodec::VP8,
|
|
..Default::default()
|
|
};
|
|
let publication = self
|
|
.publish_track(livekit::track::LocalTrack::Video(track.0), options, cx)
|
|
.await?;
|
|
|
|
Ok((publication, stream))
|
|
}
|
|
|
|
async fn publish_track(
|
|
&self,
|
|
track: livekit::track::LocalTrack,
|
|
options: livekit::options::TrackPublishOptions,
|
|
cx: &mut AsyncApp,
|
|
) -> Result<LocalTrackPublication> {
|
|
let participant = self.0.clone();
|
|
Tokio::spawn(cx, async move {
|
|
participant.publish_track(track, options).await
|
|
})?
|
|
.await?
|
|
.map(LocalTrackPublication)
|
|
.context("publishing a track")
|
|
}
|
|
|
|
pub async fn unpublish_track(
|
|
&self,
|
|
sid: TrackSid,
|
|
cx: &mut AsyncApp,
|
|
) -> Result<LocalTrackPublication> {
|
|
let participant = self.0.clone();
|
|
Tokio::spawn(cx, async move { participant.unpublish_track(&sid).await })?
|
|
.await?
|
|
.map(LocalTrackPublication)
|
|
.context("unpublishing a track")
|
|
}
|
|
}
|
|
|
|
impl LocalTrackPublication {
|
|
pub fn mute(&self, cx: &App) {
|
|
let track = self.0.clone();
|
|
Tokio::spawn(cx, async move {
|
|
track.mute();
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
pub fn unmute(&self, cx: &App) {
|
|
let track = self.0.clone();
|
|
Tokio::spawn(cx, async move {
|
|
track.unmute();
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
pub fn sid(&self) -> TrackSid {
|
|
self.0.sid()
|
|
}
|
|
|
|
pub fn is_muted(&self) -> bool {
|
|
self.0.is_muted()
|
|
}
|
|
}
|
|
|
|
impl RemoteParticipant {
|
|
pub fn identity(&self) -> ParticipantIdentity {
|
|
ParticipantIdentity(self.0.identity().0)
|
|
}
|
|
|
|
pub fn track_publications(&self) -> HashMap<TrackSid, RemoteTrackPublication> {
|
|
self.0
|
|
.track_publications()
|
|
.into_iter()
|
|
.map(|(sid, publication)| (sid, RemoteTrackPublication(publication)))
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
impl RemoteAudioTrack {
|
|
pub fn sid(&self) -> TrackSid {
|
|
self.0.sid()
|
|
}
|
|
}
|
|
|
|
impl RemoteVideoTrack {
|
|
pub fn sid(&self) -> TrackSid {
|
|
self.0.sid()
|
|
}
|
|
}
|
|
|
|
impl RemoteTrackPublication {
|
|
pub fn is_muted(&self) -> bool {
|
|
self.0.is_muted()
|
|
}
|
|
|
|
pub fn is_enabled(&self) -> bool {
|
|
self.0.is_enabled()
|
|
}
|
|
|
|
pub fn track(&self) -> Option<RemoteTrack> {
|
|
self.0.track().map(remote_track_from_livekit)
|
|
}
|
|
|
|
pub fn is_audio(&self) -> bool {
|
|
self.0.kind() == livekit::track::TrackKind::Audio
|
|
}
|
|
|
|
pub fn set_enabled(&self, enabled: bool, cx: &App) {
|
|
let track = self.0.clone();
|
|
Tokio::spawn(cx, async move { track.set_enabled(enabled) }).detach();
|
|
}
|
|
|
|
pub fn sid(&self) -> TrackSid {
|
|
self.0.sid()
|
|
}
|
|
}
|
|
|
|
impl Participant {
|
|
pub fn identity(&self) -> ParticipantIdentity {
|
|
match self {
|
|
Participant::Local(local_participant) => {
|
|
ParticipantIdentity(local_participant.0.identity().0)
|
|
}
|
|
Participant::Remote(remote_participant) => {
|
|
ParticipantIdentity(remote_participant.0.identity().0)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn participant_from_livekit(participant: livekit::participant::Participant) -> Participant {
|
|
match participant {
|
|
livekit::participant::Participant::Local(local) => {
|
|
Participant::Local(LocalParticipant(local))
|
|
}
|
|
livekit::participant::Participant::Remote(remote) => {
|
|
Participant::Remote(RemoteParticipant(remote))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn publication_from_livekit(
|
|
publication: livekit::publication::TrackPublication,
|
|
) -> TrackPublication {
|
|
match publication {
|
|
livekit::publication::TrackPublication::Local(local) => {
|
|
TrackPublication::Local(LocalTrackPublication(local))
|
|
}
|
|
livekit::publication::TrackPublication::Remote(remote) => {
|
|
TrackPublication::Remote(RemoteTrackPublication(remote))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn remote_track_from_livekit(track: livekit::track::RemoteTrack) -> RemoteTrack {
|
|
match track {
|
|
livekit::track::RemoteTrack::Audio(audio) => RemoteTrack::Audio(RemoteAudioTrack(audio)),
|
|
livekit::track::RemoteTrack::Video(video) => RemoteTrack::Video(RemoteVideoTrack(video)),
|
|
}
|
|
}
|
|
|
|
fn local_track_from_livekit(track: livekit::track::LocalTrack) -> LocalTrack {
|
|
match track {
|
|
livekit::track::LocalTrack::Audio(audio) => LocalTrack::Audio(LocalAudioTrack(audio)),
|
|
livekit::track::LocalTrack::Video(video) => LocalTrack::Video(LocalVideoTrack(video)),
|
|
}
|
|
}
|
|
fn room_event_from_livekit(event: livekit::RoomEvent) -> Option<RoomEvent> {
|
|
let event = match event {
|
|
livekit::RoomEvent::ParticipantConnected(remote_participant) => {
|
|
RoomEvent::ParticipantConnected(RemoteParticipant(remote_participant))
|
|
}
|
|
livekit::RoomEvent::ParticipantDisconnected(remote_participant) => {
|
|
RoomEvent::ParticipantDisconnected(RemoteParticipant(remote_participant))
|
|
}
|
|
livekit::RoomEvent::LocalTrackPublished {
|
|
publication,
|
|
track,
|
|
participant,
|
|
} => RoomEvent::LocalTrackPublished {
|
|
publication: LocalTrackPublication(publication),
|
|
track: local_track_from_livekit(track),
|
|
participant: LocalParticipant(participant),
|
|
},
|
|
livekit::RoomEvent::LocalTrackUnpublished {
|
|
publication,
|
|
participant,
|
|
} => RoomEvent::LocalTrackUnpublished {
|
|
publication: LocalTrackPublication(publication),
|
|
participant: LocalParticipant(participant),
|
|
},
|
|
livekit::RoomEvent::LocalTrackSubscribed { track } => RoomEvent::LocalTrackSubscribed {
|
|
track: local_track_from_livekit(track),
|
|
},
|
|
livekit::RoomEvent::TrackSubscribed {
|
|
track,
|
|
publication,
|
|
participant,
|
|
} => RoomEvent::TrackSubscribed {
|
|
track: remote_track_from_livekit(track),
|
|
publication: RemoteTrackPublication(publication),
|
|
participant: RemoteParticipant(participant),
|
|
},
|
|
livekit::RoomEvent::TrackUnsubscribed {
|
|
track,
|
|
publication,
|
|
participant,
|
|
} => RoomEvent::TrackUnsubscribed {
|
|
track: remote_track_from_livekit(track),
|
|
publication: RemoteTrackPublication(publication),
|
|
participant: RemoteParticipant(participant),
|
|
},
|
|
livekit::RoomEvent::TrackSubscriptionFailed {
|
|
participant,
|
|
error: _,
|
|
track_sid,
|
|
} => RoomEvent::TrackSubscriptionFailed {
|
|
participant: RemoteParticipant(participant),
|
|
track_sid,
|
|
},
|
|
livekit::RoomEvent::TrackPublished {
|
|
publication,
|
|
participant,
|
|
} => RoomEvent::TrackPublished {
|
|
publication: RemoteTrackPublication(publication),
|
|
participant: RemoteParticipant(participant),
|
|
},
|
|
livekit::RoomEvent::TrackUnpublished {
|
|
publication,
|
|
participant,
|
|
} => RoomEvent::TrackUnpublished {
|
|
publication: RemoteTrackPublication(publication),
|
|
participant: RemoteParticipant(participant),
|
|
},
|
|
livekit::RoomEvent::TrackMuted {
|
|
participant,
|
|
publication,
|
|
} => RoomEvent::TrackMuted {
|
|
publication: publication_from_livekit(publication),
|
|
participant: participant_from_livekit(participant),
|
|
},
|
|
livekit::RoomEvent::TrackUnmuted {
|
|
participant,
|
|
publication,
|
|
} => RoomEvent::TrackUnmuted {
|
|
publication: publication_from_livekit(publication),
|
|
participant: participant_from_livekit(participant),
|
|
},
|
|
livekit::RoomEvent::RoomMetadataChanged {
|
|
old_metadata,
|
|
metadata,
|
|
} => RoomEvent::RoomMetadataChanged {
|
|
old_metadata,
|
|
metadata,
|
|
},
|
|
livekit::RoomEvent::ParticipantMetadataChanged {
|
|
participant,
|
|
old_metadata,
|
|
metadata,
|
|
} => RoomEvent::ParticipantMetadataChanged {
|
|
participant: participant_from_livekit(participant),
|
|
old_metadata,
|
|
metadata,
|
|
},
|
|
livekit::RoomEvent::ParticipantNameChanged {
|
|
participant,
|
|
old_name,
|
|
name,
|
|
} => RoomEvent::ParticipantNameChanged {
|
|
participant: participant_from_livekit(participant),
|
|
old_name,
|
|
name,
|
|
},
|
|
livekit::RoomEvent::ParticipantAttributesChanged {
|
|
participant,
|
|
changed_attributes,
|
|
} => RoomEvent::ParticipantAttributesChanged {
|
|
participant: participant_from_livekit(participant),
|
|
changed_attributes: changed_attributes.into_iter().collect(),
|
|
},
|
|
livekit::RoomEvent::ActiveSpeakersChanged { speakers } => {
|
|
RoomEvent::ActiveSpeakersChanged {
|
|
speakers: speakers.into_iter().map(participant_from_livekit).collect(),
|
|
}
|
|
}
|
|
livekit::RoomEvent::Connected {
|
|
participants_with_tracks,
|
|
} => RoomEvent::Connected {
|
|
participants_with_tracks: participants_with_tracks
|
|
.into_iter()
|
|
.map({
|
|
|(p, t)| {
|
|
(
|
|
RemoteParticipant(p),
|
|
t.into_iter().map(RemoteTrackPublication).collect(),
|
|
)
|
|
}
|
|
})
|
|
.collect(),
|
|
},
|
|
livekit::RoomEvent::Disconnected { reason } => RoomEvent::Disconnected {
|
|
reason: reason.as_str_name(),
|
|
},
|
|
livekit::RoomEvent::Reconnecting => RoomEvent::Reconnecting,
|
|
livekit::RoomEvent::Reconnected => RoomEvent::Reconnected,
|
|
_ => {
|
|
log::trace!("dropping livekit event: {:?}", event);
|
|
return None;
|
|
}
|
|
};
|
|
|
|
Some(event)
|
|
}
|