selene-daemon 0.1.1

Official music player daemon for Selene
Documentation
use std::{
    sync::mpsc::{Receiver, Sender, TryRecvError, channel},
    thread::{self},
};

use lunar_lib::{debug, error, trace};
use ringbuf::traits::Observer;

use crate::{
    PlayerEvent,
    daemon::DaemonError,
    listener::{IpcListener, Listener},
    player::playback::{CpalError, CpalHandle, DeviceConfig},
    playlist::{Playlist, TracklistTrack},
    wait,
};

#[cfg(feature = "mpris")]
mod mpris;

#[cfg(feature = "mpris")]
use mpris::{MprisConnector, MprisEvent};

mod opened_decoder;
pub use opened_decoder::*;

pub mod playback;

mod ipc;
pub use ipc::*;

pub struct Player {
    rx: Receiver<PlayerRequest>,
    listener_tx: Sender<PlayerEvent>,

    #[cfg(feature = "mpris")]
    mpris_tx: tokio::sync::mpsc::Sender<MprisEvent>,

    playlist: Playlist,

    cpal_handle: Option<CpalHandle>,
    device_config: DeviceConfig,

    decoder: Option<OpenedDecoder>,
    preload: Option<OpenedDecoder>,

    is_playing: bool,
    volume: f32,
}

// Thread start impls
impl Player {
    fn new() -> Result<(Sender<PlayerRequest>, Self), DaemonError> {
        let (engine_tx, rx) = channel();

        let listener_tx = Listener::open(engine_tx.clone())?;

        #[cfg(feature = "mpris")]
        let mpris_tx = MprisConnector::open(engine_tx.clone());

        Ok((
            engine_tx,
            Self {
                rx,
                listener_tx,
                playlist: Playlist::new(),
                cpal_handle: None,
                device_config: DeviceConfig::default_config()?,
                decoder: None,
                preload: None,
                is_playing: false,
                volume: 1.0,

                #[cfg(feature = "mpris")]
                mpris_tx,
            },
        ))
    }

    /// Opens a new [`Player`] and runs it on a new thread
    pub fn open() -> Result<Sender<PlayerRequest>, DaemonError> {
        let (player_tx, player) = Self::new()?;

        thread::spawn(move || {
            if let Err(err) = player.run() {
                error!("Engine failed with error: {err}");
            }
        });

        Ok(player_tx)
    }

    /// Opens a new [`Player`] and runs it on the current thread
    pub fn start() -> Result<(), DaemonError> {
        let (_, player) = Self::new()?;
        player.run()
    }

    fn run(mut self) -> Result<(), DaemonError> {
        debug!("Engine thread started");

        loop {
            match self.rx.try_recv() {
                Ok(PlayerRequest::Quit) => todo!(),
                Ok(PlayerRequest::Ipc { command, callback }) => {
                    let response = self.run_command(*command)?;
                    callback.send(response)?;
                }
                Ok(PlayerRequest::Mpris { command }) => {
                    let _ = self.run_command(*command)?;
                }
                Err(TryRecvError::Empty) => (),
                Err(TryRecvError::Disconnected) => {
                    return Ok(());
                }
            };

            if !self.is_playing {
                wait();
                continue;
            }

            self.try_fill_decoders()?;
            if self.decoder.is_none() {
                wait();
                continue;
            }

            // Use the current CpalHandle, or open a new one if one doesnt exist
            self.ensure_cpal_handle()?;

            if let Some(finished_consuming) = self
                .cpal_handle
                .as_mut()
                .unwrap()
                .consume_packet(self.volume)
                && !finished_consuming
            {
                wait();
                continue;
            }

            if self.decoder.as_ref().unwrap().at_eof && !self.consume_preload()? {
                if self.cpal_handle.as_mut().unwrap().audio_buf.is_empty() {
                    self.decoder = None;
                    self.event(PlayerEvent::CurrentlyPlayingChanged {
                        currently_playing: None,
                    });
                    self.set_is_playing(false);
                    self.is_playing = false;
                    self.cpal_handle = None;
                } else {
                    wait();
                }
                continue;
            }

            let packet = self.decoder.as_mut().unwrap().decode_next_packet()?;
            self.cpal_handle.as_mut().unwrap().pending_packet = packet;
        }
    }

    fn event(&self, event: PlayerEvent) {
        trace!("Sending event: {event}");

        let _ = self.listener_tx.send(event.clone());

        #[cfg(feature = "mpris")]
        {
            let mpris_event = match event {
                PlayerEvent::CurrentlyPlayingChanged { currently_playing } => {
                    Some(MprisEvent::CurrentlyPlayingChanged { currently_playing })
                }
                PlayerEvent::IsPlayingChanged { is_playing } => {
                    let changed_at = self.decoder.as_ref().map(|d| d.time());
                    Some(MprisEvent::IsPlayingChanged {
                        is_playing,
                        changed_at,
                    })
                }
                PlayerEvent::ShuffleModeChanged { shuffle_mode } => {
                    Some(MprisEvent::ShuffleStatusChanged { shuffle_mode })
                }
                PlayerEvent::LoopModeChanged { loop_mode } => {
                    Some(MprisEvent::LoopStatusChanged { loop_mode })
                }
                PlayerEvent::VolumeChanged { volume } => Some(MprisEvent::VolumeChanged { volume }),
                PlayerEvent::SeekOccured { time } => Some(MprisEvent::Seeked { time }),
                PlayerEvent::Shutdown => Some(MprisEvent::Shutdown),
                _ => None,
            };

            if let Some(mpris_event) = mpris_event {
                let _ = self.mpris_tx.blocking_send(mpris_event);
            }
        }
    }
}

impl Player {
    /// Returns a mutable reference to the current [`CpalHandle`], opening one of one does not exist
    ///
    /// # Errors
    ///
    /// Errors if the [`CpalHandle`] fails to open
    pub fn ensure_cpal_handle(&mut self) -> Result<(), CpalError> {
        if self.cpal_handle.is_none() {
            self.cpal_handle = Some(CpalHandle::open(&self.device_config)?);
        };

        Ok(())
    }

    fn replace_decoders(&mut self) -> Result<(), DaemonError> {
        if let Some(pop_next) = self.playlist.pop_next().cloned() {
            self.load(pop_next)?;
        } else {
            self.unload();
        }

        if let Some(peek_next) = self.playlist.peek_next().cloned() {
            self.preload(peek_next)?
        } else {
            self.preload = None;
        }

        Ok(())
    }

    fn try_fill_decoders(&mut self) -> Result<(), DaemonError> {
        if self.decoder.is_none() && !self.consume_preload()? {
            if let Some(pop_next) = self.playlist.pop_next().cloned() {
                self.load(pop_next)?;
            } else {
                return Ok(());
            }
        }

        if self.preload.is_none()
            && let Some(peek_next) = self.playlist.peek_next().cloned()
        {
            self.preload(peek_next)?;
        }

        Ok(())
    }

    pub fn load(&mut self, playable: TracklistTrack) -> Result<(), DaemonError> {
        self.decoder = Some(OpenedDecoder::from_tracklist_track(
            playable.clone(),
            self.device_config.config.sample_rate() as usize,
        )?);

        self.event(PlayerEvent::CurrentlyPlayingChanged {
            currently_playing: Some(playable),
        });

        Ok(())
    }

    pub fn unload(&mut self) {
        self.decoder = None;
        self.preload = None;
        self.event(PlayerEvent::CurrentlyPlayingChanged {
            currently_playing: None,
        });
    }

    pub fn preload(&mut self, playable: TracklistTrack) -> Result<(), DaemonError> {
        self.preload = Some(OpenedDecoder::from_tracklist_track(
            playable,
            self.device_config.config.sample_rate() as usize,
        )?);
        Ok(())
    }

    pub fn take_preload_or_load(&mut self) -> Result<Option<OpenedDecoder>, DaemonError> {
        if let Some(preload) = self.preload.take() {
            self.playlist.pop_next();
            return Ok(Some(preload));
        }

        self.playlist
            .pop_next()
            .map(|p| {
                OpenedDecoder::from_tracklist_track(
                    p.clone(),
                    self.device_config.config.sample_rate() as usize,
                )
            })
            .transpose()
    }

    /// Consumes the current preload, making it the new active decoder, refilling the preload with a new track
    pub fn consume_preload(&mut self) -> Result<bool, DaemonError> {
        let state = self.decoder.is_some();
        self.decoder = self.take_preload_or_load()?;

        let currently_playing = self.decoder.as_ref().map(|d| d.decoded_from.clone());

        if self.decoder.is_some() || state {
            self.event(PlayerEvent::CurrentlyPlayingChanged { currently_playing });
        }

        if let Some(peek_next) = self.playlist.peek_next().cloned() {
            self.preload(peek_next)?
        }

        Ok(self.decoder.is_some())
    }

    /// Clears the audio buffer
    pub fn clear_audio_buf(&self) {
        if let Some(cpal_handle) = &self.cpal_handle {
            cpal_handle.clear_buf().unwrap();
        }
    }

    /// Sets or increments the volume to the input volume, clamps between `[0..1]`
    fn set_volume(&mut self, volume: f32, increment: bool) -> f32 {
        let volume = if increment {
            self.volume + volume
        } else {
            volume
        };

        self.volume = volume.clamp(0.0, 1.0);

        self.event(PlayerEvent::VolumeChanged {
            volume: self.volume,
        });

        self.volume
    }

    /// Sets if the player is allowed to play
    fn set_is_playing(&mut self, playing: bool) {
        self.is_playing = playing;

        if !playing {
            self.clear_audio_buf();
        }

        self.event(PlayerEvent::IsPlayingChanged {
            is_playing: playing,
        });
    }
}