use crate::mpv; use crate::signal::{Signal, SignalEmitter}; use event_listener::EventListener; use std::cell::{Ref, RefCell}; use url::Url; pub trait PlaybinEntry { fn url(&self) -> Url; } impl PlaybinEntry for Url { fn url(&self) -> Url { self.clone() } } // E: generic entry type pub struct Playbin { mpv: mpv::Handle, entries: RefCell>, volume_changed: SignalEmitter, muted_changed: SignalEmitter, paused_changed: SignalEmitter, current_entry_changed: SignalEmitter, entry_inserted: SignalEmitter, stopped: SignalEmitter, entry_removed: SignalEmitter, file_started: SignalEmitter, } impl Default for Playbin { fn default() -> Self { let mpv = mpv::Handle::new(); mpv.set_property("audio-client-name", "audrey").unwrap(); mpv.set_property("user-agent", crate::USER_AGENT).unwrap(); mpv.set_property("video", false).unwrap(); mpv.set_property("prefetch-playlist", true).unwrap(); mpv.set_property("gapless-audio", true).unwrap(); mpv.observe_property(0, "volume").unwrap(); mpv.observe_property(1, "mute").unwrap(); mpv.observe_property(2, "pause").unwrap(); mpv.observe_property(3, "playlist-pos").unwrap(); // "Useful to drain property changes before a new file is loaded." mpv.add_hook(0, "on_before_start_file", 0).unwrap(); Self { mpv, entries: RefCell::new(vec![]), volume_changed: Default::default(), muted_changed: Default::default(), paused_changed: Default::default(), current_entry_changed: Default::default(), entry_inserted: Default::default(), stopped: Default::default(), entry_removed: Default::default(), file_started: Default::default(), } } } impl Playbin where E: PlaybinEntry, { pub fn volume(&self) -> i64 { self.mpv.get_property("volume").unwrap() } pub fn set_volume(&self, volume: i64) { self.mpv.set_property("volume", volume).unwrap(); } pub fn muted(&self) -> bool { self.mpv.get_property("mute").unwrap() } pub fn set_muted(&self, muted: bool) { self.mpv.set_property("mute", muted).unwrap(); } pub fn paused(&self) -> bool { self.mpv.get_property("pause").unwrap() } pub fn set_paused(&self, paused: bool) { self.mpv.set_property("pause", paused).unwrap(); } pub fn position(&self) -> Option { self.mpv.get_property("time-pos").unwrap() } pub fn current_entry(&self) -> Option { self.mpv .get_property::("playlist-pos") .unwrap() .try_into() .ok() } pub fn seek(&self, _position: f64) { todo!() } pub fn next_entry(&self) { self.mpv.command(["playlist-next"]).unwrap(); } pub fn prev_entry(&self) { self.mpv.command(["playlist-prev"]).unwrap(); } pub fn play_entry(&self, index: usize) { self.mpv .command(["playlist-play-index", &index.to_string()]) .unwrap(); } pub fn entries(&self) -> Ref<'_, [E]> { Ref::map(self.entries.borrow(), Vec::as_ref) } pub fn push_entry(&self, entry: E) { let mut entries = self.entries.borrow_mut(); self.mpv .command(["loadfile", entry.url().as_str(), "append-play"]) .unwrap(); let index = entries.len(); entries.push(entry); drop(entries); self.entry_inserted.emit(self, index as usize); } pub fn insert_entry(&self, index: usize, entry: E) { let mut entries = self.entries.borrow_mut(); self.mpv .command(["loadfile", entry.url().as_str(), "insert-at-play"]) .unwrap(); entries.insert(index as usize, entry); drop(entries); self.entry_inserted.emit(self, index); } // stop playback and clear playlist pub fn stop(&self) { let mut entries = self.entries.borrow_mut(); self.mpv.command(["stop"]).unwrap(); entries.clear(); drop(entries); self.stopped.emit(self, ()); } pub fn remove_entry(&self, index: usize) { let mut entries = self.entries.borrow_mut(); self.mpv .command(["playlist-remove", &index.to_string()]) .unwrap(); entries.remove(index as usize); drop(entries); self.entry_removed.emit(self, index); } pub fn move_entry(&self, _from: usize, _to: usize) { todo!() } pub fn tick(&self) -> EventListener { let listener = self.mpv.wakeup_listener(); while let Some(event) = self.mpv.wait_event(0.0) { self.handle_event(event); } listener } fn handle_event(&self, event: mpv::Event) { match event { mpv::Event::PropertyChange(event) => match event.reply_userdata { 0 => { assert_eq!(&event.name, "volume"); self.volume_changed.emit(self, ()); dbg!(self.volume()); } 1 => { assert_eq!(&event.name, "mute"); self.muted_changed.emit(self, ()); dbg!(self.muted()); } 2 => { assert_eq!(&event.name, "pause"); self.paused_changed.emit(self, ()); dbg!(self.paused()); } 3 => { assert_eq!(&event.name, "playlist-pos"); self.current_entry_changed.emit(self, ()); dbg!(self.current_entry()); } _ => unreachable!(), }, mpv::Event::Hook(event) => match event.reply_userdata { 0 => { assert_eq!(&event.name, "on_before_start_file"); // just use this as a barrier println!("on_before_start_file triggered"); self.mpv.continue_hook(event.id).unwrap(); } _ => unreachable!(), }, mpv::Event::StartFile(_) => { // since we set up the hook before, the current song is guaranteed not to change // under our feet self.file_started.emit(self, ()); // sanity check assert_eq!( self.entries()[self.current_entry().unwrap()].url().as_str(), &self.mpv.get_property::("path").unwrap() ); } _ => println!("mpv event {:?}", event), } } pub fn volume_changed(&self) -> Signal<'_, Self, ()> { self.volume_changed.signal() } pub fn muted_changed(&self) -> Signal<'_, Self, ()> { self.muted_changed.signal() } pub fn current_entry_changed(&self) -> Signal<'_, Self, ()> { self.current_entry_changed.signal() } pub fn entry_inserted(&self) -> Signal<'_, Self, usize> { self.entry_inserted.signal() } pub fn stopped(&self) -> Signal<'_, Self, ()> { self.stopped.signal() } pub fn entry_removed(&self) -> Signal<'_, Self, usize> { self.entry_removed.signal() } pub fn file_started(&self) -> Signal<'_, Self, ()> { self.file_started.signal() } } impl Drop for Playbin { fn drop(&mut self) { println!("dropping Playbin2"); self.mpv.command(["quit"]).unwrap(); } }