use std::{
cell::RefCell,
collections::HashMap,
rc::{Rc, Weak},
};
use derive_more::{Display, From};
use medea_client_api_proto::MediaSourceKind;
use tracerr::Traced;
use crate::{
media::{
track::MediaStreamTrackState, MediaKind, MediaStreamSettings,
MultiSourceTracksConstraints,
},
platform,
utils::Caused,
};
use super::track::local;
#[derive(Caused, Clone, Debug, Display, From)]
#[cause(error = platform::Error)]
pub enum EnumerateDevicesError {
#[display("MediaDevices.enumerateDevices() failed: {_0}")]
Failed(platform::Error),
#[display("MediaManagerHandle is in detached state")]
Detached,
}
#[derive(Caused, Clone, Debug, Display, From)]
#[cause(error = platform::Error)]
pub enum EnumerateDisplaysError {
#[display("MediaDevices.enumerateDisplays() failed: {_0}")]
Failed(platform::Error),
#[display("MediaManagerHandle is in detached state")]
Detached,
}
#[derive(Caused, Clone, Debug, Display, From)]
#[cause(error = platform::Error)]
pub enum InitLocalTracksError {
#[display("MediaManagerHandle is in detached state")]
Detached,
#[display("Failed to get local tracks: {_0}")]
GetUserMediaFailed(#[cause] GetUserMediaError),
#[display("Failed to get local tracks: {_0}")]
GetDisplayMediaFailed(#[cause] GetDisplayMediaError),
}
#[derive(Clone, Copy, Debug, Display)]
#[display("Invalid audio device ID provided")]
pub struct InvalidOutputAudioDeviceIdError;
#[derive(Caused, Clone, Debug, Display, From)]
#[cause(error = platform::Error)]
pub enum MicVolumeError {
#[display("Error accessing microphone volume settings: {_0}")]
MicVolumeError(platform::Error),
#[display("`MediaManagerHandle` is in detached state")]
Detached,
}
#[derive(Clone, Copy, Debug, Display)]
#[display("MediaManagerHandle is in detached state")]
pub struct HandleDetachedError;
#[derive(Clone, Debug, Display)]
#[display("{_0} track is ended")]
struct LocalTrackIsEndedError(MediaKind);
#[derive(Caused, Clone, Debug, Display, From)]
#[cause(error = platform::Error)]
pub enum GetUserMediaError {
#[display("MediaDevices.getUserMedia() failed: {_0}")]
PlatformRequestFailed(platform::GetUserMediaError),
#[display("New {_0} local track was ended")]
LocalTrackIsEnded(MediaKind),
}
impl From<LocalTrackIsEndedError> for GetUserMediaError {
fn from(err: LocalTrackIsEndedError) -> Self {
Self::LocalTrackIsEnded(err.0)
}
}
#[expect(variant_size_differences, reason = "`Box`ing still reports this")]
#[derive(Caused, Clone, Debug, Display, From)]
#[cause(error = platform::Error)]
pub enum GetDisplayMediaError {
#[display("`MediaDevices.getDisplayMedia()` failed: {_0}")]
PlatformRequestFailed(platform::Error),
#[display("New {_0} local track was ended")]
LocalTrackIsEnded(MediaKind),
}
impl From<LocalTrackIsEndedError> for GetDisplayMediaError {
fn from(err: LocalTrackIsEndedError) -> Self {
Self::LocalTrackIsEnded(err.0)
}
}
#[derive(Debug, Default)]
pub struct MediaManager(Rc<InnerMediaManager>);
#[derive(Debug, Default)]
struct InnerMediaManager {
tracks: RefCell<HashMap<String, Weak<local::Track>>>,
media_devices: platform::MediaDevices,
}
impl InnerMediaManager {
pub fn on_device_change(&self, cb: platform::Function<()>) {
self.media_devices.on_device_change(Some(move || {
cb.call0();
}));
}
async fn enumerate_devices(
&self,
) -> Result<Vec<platform::MediaDeviceInfo>, Traced<platform::Error>> {
self.media_devices
.enumerate_devices()
.await
.map_err(tracerr::wrap!())
}
async fn enumerate_displays(
&self,
) -> Result<Vec<platform::MediaDisplayInfo>, Traced<platform::Error>> {
self.media_devices
.enumerate_displays()
.await
.map_err(tracerr::wrap!())
}
async fn get_tracks(
&self,
mut caps: MediaStreamSettings,
) -> Result<Vec<(Rc<local::Track>, bool)>, Traced<InitLocalTracksError>>
{
let tracks_from_storage = self
.get_from_storage(&mut caps)
.await
.into_iter()
.map(|t| (t, false));
match caps.into() {
None => Ok(tracks_from_storage.collect()),
Some(MultiSourceTracksConstraints::Display(caps)) => {
Ok(tracks_from_storage
.chain(
self.get_display_media(caps)
.await
.map_err(tracerr::map_from_and_wrap!())?
.into_iter()
.map(|t| (t, true)),
)
.collect())
}
Some(MultiSourceTracksConstraints::Device(caps)) => {
Ok(tracks_from_storage
.chain(
self.get_user_media(caps)
.await
.map_err(tracerr::map_from_and_wrap!())?
.into_iter()
.map(|t| (t, true)),
)
.collect())
}
Some(MultiSourceTracksConstraints::DeviceAndDisplay(
device_caps,
display_caps,
)) => {
let device_tracks = self
.get_user_media(device_caps)
.await
.map_err(tracerr::map_from_and_wrap!())?;
let display_tracks = self
.get_display_media(display_caps)
.await
.map_err(tracerr::map_from_and_wrap!())?;
Ok(tracks_from_storage
.chain(
device_tracks
.into_iter()
.chain(display_tracks.into_iter())
.map(|t| (t, true)),
)
.collect())
}
}
}
async fn get_from_storage(
&self,
caps: &mut MediaStreamSettings,
) -> Vec<Rc<local::Track>> {
self.tracks
.borrow_mut()
.retain(|_, track| Weak::strong_count(track) > 0);
#[expect( // intentional
clippy::unwrap_used,
reason = "absent ones are cleaned in the line above"
)]
let storage: Vec<_> = self
.tracks
.borrow()
.iter()
.map(|(_, track)| Weak::upgrade(track).unwrap())
.collect();
let mut tracks = Vec::new();
if caps.is_audio_enabled() {
for track in &storage {
if caps.get_audio().satisfies(track.as_ref()).await {
caps.set_audio_publish(false);
tracks.push(Rc::clone(track));
break;
}
}
}
for track in storage {
if caps.unconstrain_if_satisfies_video(track.as_ref()).await {
tracks.push(track);
}
}
tracks
}
async fn get_user_media(
&self,
caps: platform::MediaStreamConstraints,
) -> Result<Vec<Rc<local::Track>>, Traced<GetUserMediaError>> {
let tracks = self
.media_devices
.get_user_media(caps)
.await
.map_err(tracerr::map_from_and_wrap!())?;
let tracks = self
.parse_and_save_tracks(tracks, MediaSourceKind::Device)
.await
.map_err(tracerr::map_from_and_wrap!())?;
Ok(tracks)
}
async fn get_display_media(
&self,
caps: platform::DisplayMediaStreamConstraints,
) -> Result<Vec<Rc<local::Track>>, Traced<GetDisplayMediaError>> {
let tracks = self
.media_devices
.get_display_media(caps)
.await
.map_err(tracerr::map_from_and_wrap!())?;
let track = self
.parse_and_save_tracks(tracks, MediaSourceKind::Display)
.await
.map_err(tracerr::map_from_and_wrap!())?;
Ok(track)
}
async fn parse_and_save_tracks(
&self,
tracks: Vec<platform::MediaStreamTrack>,
kind: MediaSourceKind,
) -> Result<Vec<Rc<local::Track>>, Traced<LocalTrackIsEndedError>> {
for track in &tracks {
if track.ready_state().await != MediaStreamTrackState::Live {
return Err(tracerr::new!(LocalTrackIsEndedError(
track.kind()
)));
}
}
let mut storage = self.tracks.borrow_mut();
let tracks = tracks
.into_iter()
.map(|tr| Rc::new(local::Track::new(tr, kind)))
.inspect(|track| {
drop(storage.insert(track.id(), Rc::downgrade(track)));
})
.collect();
Ok(tracks)
}
async fn set_output_audio_id(
&self,
device_id: String,
) -> Result<(), Traced<InvalidOutputAudioDeviceIdError>> {
#[expect(clippy::map_err_ignore, reason = "not useful")]
self.media_devices
.set_output_audio_id(device_id)
.await
.map_err(|_| tracerr::new!(InvalidOutputAudioDeviceIdError))
}
async fn microphone_volume_is_available(&self) -> bool {
self.media_devices.microphone_volume_is_available().await
}
async fn set_microphone_volume(
&self,
level: i64,
) -> Result<(), Traced<MicVolumeError>> {
self.media_devices
.set_microphone_volume(level)
.await
.map_err(tracerr::map_from_and_wrap!())
}
async fn microphone_volume(&self) -> Result<i64, Traced<MicVolumeError>> {
self.media_devices
.microphone_volume()
.await
.map_err(tracerr::map_from_and_wrap!())
}
}
impl MediaManager {
pub async fn get_tracks<I: Into<MediaStreamSettings>>(
&self,
caps: I,
) -> Result<Vec<(Rc<local::Track>, bool)>, Traced<InitLocalTracksError>>
{
self.0
.get_tracks(caps.into())
.await
.map_err(tracerr::wrap!())
}
#[must_use]
pub fn new_handle(&self) -> MediaManagerHandle {
MediaManagerHandle(Rc::downgrade(&self.0))
}
}
#[derive(Clone, Debug)]
pub struct MediaManagerHandle(Weak<InnerMediaManager>);
impl MediaManagerHandle {
pub async fn enumerate_devices(
&self,
) -> Result<Vec<platform::MediaDeviceInfo>, Traced<EnumerateDevicesError>>
{
let this = self
.0
.upgrade()
.ok_or_else(|| tracerr::new!(EnumerateDevicesError::Detached))?;
this.enumerate_devices()
.await
.map_err(tracerr::map_from_and_wrap!())
}
pub async fn enumerate_displays(
&self,
) -> Result<Vec<platform::MediaDisplayInfo>, Traced<EnumerateDisplaysError>>
{
let this = self
.0
.upgrade()
.ok_or_else(|| tracerr::new!(EnumerateDisplaysError::Detached))?;
this.enumerate_displays()
.await
.map_err(tracerr::map_from_and_wrap!())
}
pub async fn init_local_tracks(
&self,
caps: MediaStreamSettings,
) -> Result<Vec<local::LocalMediaTrack>, Traced<InitLocalTracksError>> {
let this = self
.0
.upgrade()
.ok_or_else(|| tracerr::new!(InitLocalTracksError::Detached))?;
this.get_tracks(caps)
.await
.map(|tracks| {
tracks
.into_iter()
.map(|(t, _)| local::LocalMediaTrack::new(t))
.collect::<Vec<_>>()
})
.map_err(tracerr::map_from_and_wrap!())
}
pub async fn set_output_audio_id(
&self,
device_id: String,
) -> Result<(), Traced<InvalidOutputAudioDeviceIdError>> {
let this = self
.0
.upgrade()
.ok_or_else(|| tracerr::new!(InvalidOutputAudioDeviceIdError))?;
this.set_output_audio_id(device_id)
.await
.map_err(tracerr::map_from_and_wrap!())
}
pub async fn set_microphone_volume(
&self,
level: i64,
) -> Result<(), Traced<MicVolumeError>> {
let this = self
.0
.upgrade()
.ok_or_else(|| tracerr::new!(MicVolumeError::Detached))?;
this.set_microphone_volume(level)
.await
.map_err(tracerr::map_from_and_wrap!())
}
pub async fn microphone_volume_is_available(
&self,
) -> Result<bool, Traced<HandleDetachedError>> {
let this = self
.0
.upgrade()
.ok_or_else(|| tracerr::new!(HandleDetachedError))?;
Ok(this.microphone_volume_is_available().await)
}
pub async fn microphone_volume(
&self,
) -> Result<i64, Traced<MicVolumeError>> {
let this = self
.0
.upgrade()
.ok_or_else(|| tracerr::new!(MicVolumeError::Detached))?;
this.microphone_volume()
.await
.map_err(tracerr::map_from_and_wrap!())
}
pub fn on_device_change(
&self,
cb: platform::Function<()>,
) -> Result<(), Traced<HandleDetachedError>> {
let this = self
.0
.upgrade()
.ok_or_else(|| tracerr::new!(HandleDetachedError))?;
this.on_device_change(cb);
Ok(())
}
}