#[cfg(feature = "audio")]
mod backend {
use std::cell::RefCell;
use std::path::Path;
use std::rc::Rc;
use std::sync::Once;
use gstreamer as gst;
use gstreamer::prelude::*;
use relm4::gtk::{gio, glib};
use relm4::gtk::gio::prelude::FileExt;
static INIT: Once = Once::new();
pub struct AudioPlayer {
pipeline: gst::Element,
_bus_watch: RefCell<Option<gst::bus::BusWatchGuard>>,
}
impl AudioPlayer {
pub fn new(path: &Path) -> Option<Rc<Self>> {
INIT.call_once(|| {
let _ = gst::init();
});
let pipeline = match gst::ElementFactory::make("playbin").build() {
Ok(pipeline) => pipeline,
Err(err) => {
eprintln!("tree-space: playbin unavailable: {err}");
return None;
}
};
pipeline.set_property("uri", gio::File::for_path(path).uri().as_str());
pipeline.set_property("mute", false);
pipeline.set_property("volume", 1.0);
let player = Rc::new(AudioPlayer {
pipeline,
_bus_watch: RefCell::new(None),
});
player.install_bus_watch();
Some(player)
}
fn install_bus_watch(self: &Rc<Self>) {
let Some(bus) = self.pipeline.bus() else {
return;
};
let weak = Rc::downgrade(self);
let watch = bus.add_watch_local(move |_, message| {
use gst::MessageView;
let Some(player) = weak.upgrade() else {
return glib::ControlFlow::Break;
};
match message.view() {
MessageView::Eos(..) => {
let _ = player.pipeline.set_state(gst::State::Paused);
let _ = player.pipeline.seek_simple(
gst::SeekFlags::FLUSH | gst::SeekFlags::KEY_UNIT,
gst::ClockTime::ZERO,
);
}
MessageView::Error(err) => {
eprintln!("tree-space: audio playback error: {}", err.error());
}
_ => {}
}
glib::ControlFlow::Continue
});
if let Ok(guard) = watch {
*self._bus_watch.borrow_mut() = Some(guard);
}
}
pub fn is_playing(&self) -> bool {
self.pipeline.current_state() == gst::State::Playing
}
pub fn set_playing(&self, playing: bool) {
let state = if playing { gst::State::Playing } else { gst::State::Paused };
let _ = self.pipeline.set_state(state);
}
pub fn position(&self) -> i64 {
self.pipeline
.query_position::<gst::ClockTime>()
.map(|t| t.useconds() as i64)
.unwrap_or(0)
}
pub fn duration(&self) -> i64 {
self.pipeline
.query_duration::<gst::ClockTime>()
.map(|t| t.useconds() as i64)
.unwrap_or(0)
}
pub fn seek(&self, microseconds: i64) {
if microseconds <= 0 {
return;
}
let _ = self.pipeline.seek_simple(
gst::SeekFlags::FLUSH | gst::SeekFlags::KEY_UNIT,
gst::ClockTime::from_useconds(microseconds as u64),
);
}
pub fn set_volume(&self, volume: f64) {
self.pipeline.set_property("volume", volume.clamp(0.0, 1.0));
}
pub fn volume(&self) -> f64 {
self.pipeline.property::<f64>("volume")
}
}
impl Drop for AudioPlayer {
fn drop(&mut self) {
let _ = self.pipeline.set_state(gst::State::Null);
}
}
}
#[cfg(not(feature = "audio"))]
mod backend {
use std::path::Path;
use std::rc::Rc;
pub struct AudioPlayer;
impl AudioPlayer {
pub fn new(_path: &Path) -> Option<Rc<Self>> {
None
}
pub fn is_playing(&self) -> bool {
false
}
pub fn set_playing(&self, _playing: bool) {}
pub fn position(&self) -> i64 {
0
}
pub fn duration(&self) -> i64 {
0
}
pub fn seek(&self, _microseconds: i64) {}
pub fn set_volume(&self, _volume: f64) {}
pub fn volume(&self) -> f64 {
1.0
}
}
}
pub use backend::AudioPlayer;