Skip to main content

tree_space/
audio.rs

1//! Inline audio playback.
2//!
3//! Why a private `playbin` instead of [`gtk::MediaFile`]? GTK's media backend is
4//! built on GstPlay, which hardcodes the `playbin3` pipeline and attaches its GL
5//! video sink. On some systems that combination races `decodebin3`'s handling of
6//! `id3demux` for mp3 files carrying an ID3v2 tag and aborts the whole process
7//! (`gstdecodebin3.c: mq_slot_handle_stream_start: assertion failed:
8//! (collection)`), deterministically and even without pressing play. `playbin`
9//! (v1) uses `decodebin`, which handles the same files fine, so audio previews
10//! go through it instead. Video previews keep using `GtkMediaFile`.
11//!
12//! The [`gstreamer`](https://crates.io/crates/gstreamer) binding is optional
13//! (the `audio` Cargo feature, on by default). With it disabled, [`AudioPlayer`]
14//! is a stub whose [`AudioPlayer::new`] always returns `None`, and no audio file
15//! is offered an inline player.
16
17#[cfg(feature = "audio")]
18mod backend {
19    use std::cell::RefCell;
20    use std::path::Path;
21    use std::rc::Rc;
22    use std::sync::Once;
23
24    use gstreamer as gst;
25    use gstreamer::prelude::*;
26    use relm4::gtk::{gio, glib};
27    use relm4::gtk::gio::prelude::FileExt;
28
29    static INIT: Once = Once::new();
30
31    /// One audio stream playing through a private `playbin` element.
32    pub struct AudioPlayer {
33        pipeline: gst::Element,
34        /// Kept alive (and dropped) to keep the bus watch installed.
35        _bus_watch: RefCell<Option<gst::bus::BusWatchGuard>>,
36    }
37
38    impl AudioPlayer {
39        /// Build a paused-ready player for `path`, or `None` if `playbin` is
40        /// unavailable. The pipeline stays un-prerolled until playback starts,
41        /// so a directory with many audio files does not spin up a sink each.
42        pub fn new(path: &Path) -> Option<Rc<Self>> {
43            INIT.call_once(|| {
44                let _ = gst::init();
45            });
46
47            let pipeline = match gst::ElementFactory::make("playbin").build() {
48                Ok(pipeline) => pipeline,
49                Err(err) => {
50                    eprintln!("tree-space: playbin unavailable: {err}");
51                    return None;
52                }
53            };
54            pipeline.set_property("uri", gio::File::for_path(path).uri().as_str());
55            // Match the video path: audible, with the panel's volume control.
56            pipeline.set_property("mute", false);
57            pipeline.set_property("volume", 1.0);
58
59            let player = Rc::new(AudioPlayer {
60                pipeline,
61                _bus_watch: RefCell::new(None),
62            });
63            player.install_bus_watch();
64            Some(player)
65        }
66
67        /// Watch the bus for end-of-stream and errors. On EOS the stream is
68        /// rewound and paused so pressing play again starts from the top.
69        fn install_bus_watch(self: &Rc<Self>) {
70            let Some(bus) = self.pipeline.bus() else {
71                return;
72            };
73            let weak = Rc::downgrade(self);
74            let watch = bus.add_watch_local(move |_, message| {
75                use gst::MessageView;
76                let Some(player) = weak.upgrade() else {
77                    return glib::ControlFlow::Break;
78                };
79                match message.view() {
80                    MessageView::Eos(..) => {
81                        let _ = player.pipeline.set_state(gst::State::Paused);
82                        let _ = player.pipeline.seek_simple(
83                            gst::SeekFlags::FLUSH | gst::SeekFlags::KEY_UNIT,
84                            gst::ClockTime::ZERO,
85                        );
86                    }
87                    MessageView::Error(err) => {
88                        eprintln!("tree-space: audio playback error: {}", err.error());
89                    }
90                    _ => {}
91                }
92                glib::ControlFlow::Continue
93            });
94            if let Ok(guard) = watch {
95                *self._bus_watch.borrow_mut() = Some(guard);
96            }
97        }
98
99        pub fn is_playing(&self) -> bool {
100            self.pipeline.current_state() == gst::State::Playing
101        }
102
103        pub fn set_playing(&self, playing: bool) {
104            let state = if playing { gst::State::Playing } else { gst::State::Paused };
105            let _ = self.pipeline.set_state(state);
106        }
107
108        /// Current position in microseconds (0 while unknown).
109        pub fn position(&self) -> i64 {
110            self.pipeline
111                .query_position::<gst::ClockTime>()
112                .map(|t| t.useconds() as i64)
113                .unwrap_or(0)
114        }
115
116        /// Stream duration in microseconds (0 while unknown).
117        pub fn duration(&self) -> i64 {
118            self.pipeline
119                .query_duration::<gst::ClockTime>()
120                .map(|t| t.useconds() as i64)
121                .unwrap_or(0)
122        }
123
124        /// Seek to `microseconds` from the start.
125        pub fn seek(&self, microseconds: i64) {
126            if microseconds <= 0 {
127                return;
128            }
129            let _ = self.pipeline.seek_simple(
130                gst::SeekFlags::FLUSH | gst::SeekFlags::KEY_UNIT,
131                gst::ClockTime::from_useconds(microseconds as u64),
132            );
133        }
134
135        pub fn set_volume(&self, volume: f64) {
136            self.pipeline.set_property("volume", volume.clamp(0.0, 1.0));
137        }
138
139        pub fn volume(&self) -> f64 {
140            self.pipeline.property::<f64>("volume")
141        }
142    }
143
144    impl Drop for AudioPlayer {
145        fn drop(&mut self) {
146            // Return the pipeline to NULL so its sink is released promptly.
147            let _ = self.pipeline.set_state(gst::State::Null);
148        }
149    }
150}
151
152#[cfg(not(feature = "audio"))]
153mod backend {
154    use std::path::Path;
155    use std::rc::Rc;
156
157    /// Stub used when the `audio` feature is disabled: audio files are not
158    /// offered an inline player.
159    pub struct AudioPlayer;
160
161    impl AudioPlayer {
162        pub fn new(_path: &Path) -> Option<Rc<Self>> {
163            None
164        }
165
166        pub fn is_playing(&self) -> bool {
167            false
168        }
169
170        pub fn set_playing(&self, _playing: bool) {}
171
172        pub fn position(&self) -> i64 {
173            0
174        }
175
176        pub fn duration(&self) -> i64 {
177            0
178        }
179
180        pub fn seek(&self, _microseconds: i64) {}
181
182        pub fn set_volume(&self, _volume: f64) {}
183
184        pub fn volume(&self) -> f64 {
185            1.0
186        }
187    }
188}
189
190pub use backend::AudioPlayer;