Skip to main content

mpv_engine/
engine.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2use std::sync::{Arc, Once};
3
4use parking_lot::Mutex;
5use rsmpv::{EndFileReason, Event, Format, Mpv, PropertyData, sys};
6
7use crate::error::{Error, Result, describe_code};
8use crate::render::{
9    GlRender, GlRenderOptions, ProcAddressFn, RenderBackend, RenderKind, SwRender,
10};
11
12/// Force `LC_NUMERIC=C` exactly once before the first `mpv_create`. mpv
13/// refuses to work under a comma-decimal locale (its option/number parsing
14/// breaks), and toolkits like GTK call `setlocale()` from the environment
15/// during init — so this must run even when the host app never touches
16/// locale itself. Both parent projects carried this guard independently;
17/// it lives here now.
18///
19/// The category constant comes from `libc` because its value is
20/// platform-specific: `LC_NUMERIC` is 1 on glibc but 4 on BSD/macOS and
21/// Windows — a hardcoded 1 would silently set `LC_COLLATE` there.
22fn ensure_c_numeric_locale() {
23    static ONCE: Once = Once::new();
24    ONCE.call_once(|| unsafe {
25        libc::setlocale(libc::LC_NUMERIC, c"C".as_ptr());
26    });
27}
28
29/// Playback lifecycle notifications drained by [`Engine::pump_events`].
30///
31/// Non-exhaustive: match with a wildcard arm — new variants are additive,
32/// not breaking.
33#[derive(Debug, Clone, PartialEq)]
34#[non_exhaustive]
35pub enum PlaybackEvent {
36    /// A file finished loading and playback is starting.
37    Loaded,
38    /// Playback ended without error. Errored ends arrive as
39    /// [`Failed`](Self::Failed) instead.
40    Ended {
41        /// Why it ended — playlist logic usually advances on
42        /// [`EndReason::Eof`] and holds on [`EndReason::Stop`].
43        reason: EndReason,
44    },
45    /// Playback (re)started after a seek completed or a file began
46    /// playing: the displayed frame matches `time-pos` again. Snap
47    /// scrubber/position UI here, not while a seek is still in flight.
48    PlaybackRestart,
49    /// An observed property changed ([`Engine::observe`]); also fires
50    /// once with the current value right after observation starts.
51    PropertyChanged {
52        /// Which observation this notification belongs to — matches the
53        /// [`ObserveId`] returned by [`Engine::observe`], so two
54        /// observations of the same property stay distinguishable.
55        id: ObserveId,
56        /// The mpv property name, as passed to [`Engine::observe`].
57        name: String,
58        /// The new value, in the [`PropertyFormat`] the observation chose.
59        value: PropertyValue,
60    },
61    /// The mpv core is shutting down — e.g. a `quit` issued through
62    /// [`Engine::command`], or an input binding if input was enabled.
63    /// No further events follow; drop the [`Engine`] soon. (A file that
64    /// was playing also gets an [`Ended`](Self::Ended) with
65    /// [`EndReason::Quit`] first.)
66    Shutdown,
67    /// mpv aborted playback — unreadable, corrupt, empty, or an
68    /// unrecognized/unsupported format.
69    Failed {
70        /// Raw `client.h` `mpv_error` code (always negative). Match on
71        /// this to map failures to your own user-facing error copy.
72        code: i32,
73        /// Diagnostic text (mpv's `mpv_error_string` plus the code), not
74        /// user-facing copy.
75        message: String,
76    },
77}
78
79/// Why playback ended (mpv's `mpv_end_file_reason`).
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum EndReason {
82    /// Natural end of file.
83    Eof,
84    /// `stop` command or equivalent — user intent, don't auto-advance.
85    Stop,
86    /// The player is quitting.
87    Quit,
88    /// The entry redirected to another (e.g. a playlist file).
89    Redirect,
90    /// A reason this crate doesn't recognize; carries mpv's raw code.
91    Other(i32),
92}
93
94fn end_reason(reason: EndFileReason) -> EndReason {
95    match reason {
96        EndFileReason::Eof => EndReason::Eof,
97        EndFileReason::Stop => EndReason::Stop,
98        EndFileReason::Quit => EndReason::Quit,
99        EndFileReason::Redirect => EndReason::Redirect,
100        EndFileReason::Unknown(code) => EndReason::Other(code),
101        // `Error` is routed to `Failed` before this runs, and a future
102        // named reason (the enum is non_exhaustive) carries no raw code
103        // to forward — mpv's reason codes are non-negative, so -1 is
104        // recognizably out-of-band until this crate names the variant.
105        _ => EndReason::Other(-1),
106    }
107}
108
109/// Owned property value delivered by
110/// [`PropertyChanged`](PlaybackEvent::PropertyChanged).
111#[derive(Debug, Clone, PartialEq)]
112#[non_exhaustive]
113pub enum PropertyValue {
114    /// An mpv flag (`MPV_FORMAT_FLAG`).
115    Flag(bool),
116    /// A 64-bit integer (`MPV_FORMAT_INT64`).
117    Int(i64),
118    /// A double (`MPV_FORMAT_DOUBLE`).
119    Double(f64),
120    /// A string (`MPV_FORMAT_STRING`).
121    Str(String),
122}
123
124/// Map an event's decoded property payload onto this crate's owned value.
125/// `None` when the property is unavailable or the format is one this crate
126/// doesn't deliver (e.g. `Node`) — those notifications are skipped, not
127/// surfaced as bogus values.
128fn property_value(data: PropertyData) -> Option<PropertyValue> {
129    match data {
130        PropertyData::Flag(v) => Some(PropertyValue::Flag(v)),
131        PropertyData::Int64(v) => Some(PropertyValue::Int(v)),
132        PropertyData::Double(v) => Some(PropertyValue::Double(v)),
133        PropertyData::String(s) | PropertyData::OsdString(s) => Some(PropertyValue::Str(s)),
134        _ => None,
135    }
136}
137
138impl From<bool> for PropertyValue {
139    fn from(v: bool) -> Self {
140        Self::Flag(v)
141    }
142}
143impl From<i64> for PropertyValue {
144    fn from(v: i64) -> Self {
145        Self::Int(v)
146    }
147}
148impl From<i32> for PropertyValue {
149    fn from(v: i32) -> Self {
150        Self::Int(v.into())
151    }
152}
153impl From<u32> for PropertyValue {
154    fn from(v: u32) -> Self {
155        Self::Int(v.into())
156    }
157}
158impl From<f64> for PropertyValue {
159    fn from(v: f64) -> Self {
160        Self::Double(v)
161    }
162}
163impl From<&str> for PropertyValue {
164    fn from(v: &str) -> Self {
165        Self::Str(v.to_owned())
166    }
167}
168impl From<String> for PropertyValue {
169    fn from(v: String) -> Self {
170        Self::Str(v)
171    }
172}
173
174mod sealed {
175    use crate::error::Result;
176
177    pub trait PropertyGetImpl: Sized {
178        fn get_property(mpv: &rsmpv::Mpv, name: &str) -> Result<Self>;
179    }
180
181    macro_rules! impl_property_get {
182        ($($t:ty),*) => {$(
183            impl PropertyGetImpl for $t {
184                fn get_property(mpv: &rsmpv::Mpv, name: &str) -> Result<Self> {
185                    Ok(mpv.get_property(name)?)
186                }
187            }
188        )*};
189    }
190    impl_property_get!(bool, i64, f64, String);
191}
192
193/// Types a property can be read as: `bool`, `i64`, `f64`, `String`.
194/// Sealed to the formats mpv's property API speaks — the binding's own
195/// conversion traits are deliberately not part of this crate's API, so
196/// a binding major bump can't be a breaking change here.
197pub trait PropertyGet: sealed::PropertyGetImpl {}
198impl PropertyGet for bool {}
199impl PropertyGet for i64 {}
200impl PropertyGet for f64 {}
201impl PropertyGet for String {}
202
203/// Wire format for [`Engine::observe`] — picks which [`PropertyValue`]
204/// variant change notifications carry (mpv coerces where it can).
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub enum PropertyFormat {
207    /// Deliver as [`PropertyValue::Flag`].
208    Flag,
209    /// Deliver as [`PropertyValue::Int`].
210    Int,
211    /// Deliver as [`PropertyValue::Double`].
212    Double,
213    /// Deliver as [`PropertyValue::Str`].
214    Str,
215}
216
217/// The shell's render-update callback as held in the engine's relay
218/// slot (see [`Engine::render_update_cb`]).
219type UpdateCallback = Arc<dyn Fn() + Send + Sync>;
220
221/// Handle returned by [`Engine::observe`]: tags that observation's
222/// [`PropertyChanged`](PlaybackEvent::PropertyChanged) events and cancels
223/// it via [`Engine::unobserve`].
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225pub struct ObserveId(u64);
226
227/// Configures and creates an [`Engine`]. Properties set here are applied
228/// before `mpv_initialize`, which some options require.
229pub struct EngineBuilder {
230    props: Vec<(String, String)>,
231}
232
233impl EngineBuilder {
234    /// Set an mpv property/option before initialization.
235    pub fn property(mut self, name: &str, value: &str) -> Self {
236        self.props.push((name.into(), value.into()));
237        self
238    }
239
240    /// Create the engine: forces `LC_NUMERIC=C`, applies the queued
241    /// properties, and runs `mpv_initialize`.
242    pub fn build(self) -> Result<Engine> {
243        ensure_c_numeric_locale();
244        let mut builder = Mpv::builder()?;
245        for (name, value) in &self.props {
246            builder = builder.set_property(name, value.as_str())?;
247        }
248        let mpv = Arc::new(builder.build()?);
249        Ok(Engine {
250            render: Mutex::new(None),
251            attach: Mutex::new(()),
252            pump: Mutex::new(()),
253            next_observe_id: AtomicU64::new(1),
254            pending_load: Mutex::new(None),
255            deferred_events: Mutex::new(Vec::new()),
256            render_update_cb: Arc::new(Mutex::new(Arc::new(|| {}) as UpdateCallback)),
257            mpv,
258        })
259    }
260}
261
262/// One embedded mpv player core.
263///
264/// Toolkit-agnostic by construction: no main-loop integration, no widget,
265/// no GL context of its own. A shell attaches a render target with
266/// [`attach_gl_render`](Engine::attach_gl_render), forwards the update
267/// callback to its own main loop, and drains
268/// [`pump_events`](Engine::pump_events) on whatever cadence suits it.
269pub struct Engine {
270    /// Live render context (either backend), if any. Freed-before-
271    /// terminate ordering is structural: the context co-owns the core
272    /// through its own `Arc<Mpv>`, so the core cannot terminate under a
273    /// live context regardless of field order.
274    render: Mutex<Option<RenderBackend>>,
275    /// Serializes attach calls: concurrent `mpv_render_context_create`
276    /// on one handle violates render.h's threading rules, and slot
277    /// re-checks alone can't prevent the double-create. Deliberately not
278    /// the `render` lock — the synchronous `on_update` during create
279    /// must stay free to touch render methods.
280    attach: Mutex<()>,
281    /// Keeps each [`pump_events`](Engine::pump_events) drain atomic.
282    /// rsmpv's `poll_event` is safe to call concurrently, but concurrent
283    /// pollers *split* the stream (each event goes to exactly one
284    /// caller) — two racing pumps would tear ordered sequences like
285    /// `Loaded` → `Ended` across their result batches.
286    pump: Mutex<()>,
287    /// Userdata ids handed to `mpv_observe_property`; each observation
288    /// gets a fresh one so [`unobserve`](Engine::unobserve) is precise.
289    next_observe_id: AtomicU64,
290    /// Source queued by [`load_when_ready`](Engine::load_when_ready)
291    /// until a render context attaches; the attach methods take it and
292    /// issue the `loadfile`. Cleared by any transport call that decides
293    /// what plays — `load`/`load_paused`/`stop`, or their spellings
294    /// through [`command`](Engine::command) — the newest one wins.
295    ///
296    /// Doubles as the transport-ordering lock: every such call mutates
297    /// the slot and issues its mpv command *under this guard*, so
298    /// "newest wins" is real under concurrency — a drain can't interleave
299    /// with a `stop` between taking the slot and the `loadfile` landing.
300    /// Leaf lock: nothing acquired while it is held (mpv commands take no
301    /// engine locks; wakeup callbacks are documented to call no engine
302    /// methods).
303    pending_load: Mutex<Option<String>>,
304    /// Engine-synthesized events prepended by
305    /// [`pump_events`](Engine::pump_events) — currently only a `Failed`
306    /// when a deferred load errors at attach time, so the attach methods'
307    /// `Err` can keep meaning "no context was attached".
308    deferred_events: Mutex<Vec<PlaybackEvent>>,
309    /// The live render-update callback, behind the relay closure that is
310    /// what actually gets registered with rsmpv at attach.
311    /// [`set_render_update_callback`](Engine::set_render_update_callback)
312    /// swaps this slot instead of re-registering through FFI, so the
313    /// swap holds no engine lock across a callback invocation — the
314    /// relay clones the `Arc` out under a short lock and invokes with
315    /// the lock released. All mutation goes through
316    /// [`set_update_slot`](Engine::set_update_slot), which drops the
317    /// displaced closure outside the lock.
318    render_update_cb: Arc<Mutex<UpdateCallback>>,
319    /// Shared with any live render context, which holds its own clone.
320    mpv: Arc<Mpv>,
321}
322
323// `Engine: Send + Sync` is auto-derived: rsmpv marks `Mpv` Send + Sync
324// (libmpv is thread-safe per client.h; the one caveat, single-waiter
325// `mpv_wait_event`, is upheld inside rsmpv's `poll_event`), the render
326// contexts are `Send` behind a `Sync` mutex, and the remaining fields
327// are sync primitives. Do not add manual `unsafe impl`s — they'd
328// silence the compiler if a future field is genuinely `!Send`.
329
330impl Engine {
331    /// Neutral builder with no properties preset — for consumers whose
332    /// configuration doesn't start from [`video`](Self::video) or
333    /// [`headless`](Self::headless). (The presets can also be overridden:
334    /// properties apply in call order.)
335    pub fn builder() -> EngineBuilder {
336        EngineBuilder { props: vec![] }
337    }
338
339    /// Builder preset for video playback via the libmpv render API.
340    /// Frames appear only after [`attach_gl_render`](Self::attach_gl_render);
341    /// see that method's note on load ordering.
342    pub fn video() -> EngineBuilder {
343        Self::builder()
344            .property("vo", "libmpv")
345            .property("hwdec", "auto-safe")
346            .property("keep-open", "always")
347    }
348
349    /// Builder preset for audio-only / headless use (`vo=null`): playback
350    /// starts as soon as [`load`](Self::load) runs, no render target needed.
351    /// Also what the test suite uses — no display required.
352    pub fn headless() -> EngineBuilder {
353        Self::builder()
354            .property("vo", "null")
355            .property("keep-open", "always")
356    }
357
358    /// Load a file path or URL and start playback.
359    ///
360    /// For video engines, prefer [`load_when_ready`](Self::load_when_ready)
361    /// until the shell's surface is mapped: `loadfile` before a render
362    /// context exists fails VO init and **drops the video track** (see
363    /// `load_when_ready`'s docs for the full failure). Loading paused
364    /// doesn't dodge it — the load itself is what fails — which is why
365    /// the deferred variant exists and why
366    /// [`load_paused`](Self::load_paused) is no pre-attach alternative.
367    pub fn load(&self, source: &str) -> Result<()> {
368        // A plain load supersedes any pending deferred load; the guard is
369        // held across the command so the two are one transport step.
370        let mut pending = self.pending_load.lock();
371        *pending = None;
372        self.loadfile(source)
373    }
374
375    /// Issue the raw `loadfile` command, touching no engine state. The
376    /// callers own the [`pending_load`](Self::pending_load) transport
377    /// step around this.
378    fn loadfile(&self, source: &str) -> Result<()> {
379        // rsmpv passes args as an array (`mpv_command`), so paths with
380        // spaces/quotes need no escaping here. Do not "simplify" this
381        // into a formatted command string — that reintroduces the quoting
382        // bug this crate's regression test pins.
383        self.mpv.command(&["loadfile", source])?;
384        Ok(())
385    }
386
387    /// [`load`](Self::load), but paused: pause is set *before* `loadfile`
388    /// so demuxing/audio don't start before the shell is ready. Call
389    /// [`set_paused`](Self::set_paused)`(false)` on your window-ready
390    /// signal. (Setting `pause` at init time instead has a tendency to
391    /// hang — this runtime-property ordering is the reliable variant.)
392    pub fn load_paused(&self, source: &str) -> Result<()> {
393        self.set_paused(true)?;
394        self.load(source)
395    }
396
397    /// [`load`](Self::load), deferred until frames have somewhere to go:
398    /// on a render-API engine (`vo=libmpv`, [`Engine::video`]) with no
399    /// context attached yet, the source is queued and the attach call
400    /// ([`attach_gl_render`](Self::attach_gl_render) /
401    /// [`attach_sw_render`](Self::attach_sw_render)) issues the
402    /// `loadfile` — the ordering a video shell wants, without
403    /// hand-carrying a pending-source slot between its load path and its
404    /// realize handler. On an engine that is already attached — or whose
405    /// `vo` never uses the render API ([`headless`](Self::headless), a
406    /// windowed vo), so no attach is coming — this is plain
407    /// [`load`](Self::load).
408    ///
409    /// The whole `loadfile` is deferred, not just an unpause, because a
410    /// load before the render context exists doesn't merely start
411    /// blind: mpv fails to initialize the video output and **drops the
412    /// video track** — a video-only file dies with
413    /// `MPV_ERROR_NOTHING_TO_PLAY` (-16) even when loaded paused, and a
414    /// file with audio plays sound over a permanently black surface.
415    ///
416    /// The queued source is a *pending intent*: a later
417    /// [`load`](Self::load), [`load_paused`](Self::load_paused), or
418    /// [`stop`](Self::stop) before the attach supersedes it (newest
419    /// transport call wins — including the same commands issued through
420    /// [`command`](Self::command)), and a second `load_when_ready`
421    /// replaces it. Pause
422    /// state needs no special casing — the `pause` property persists
423    /// across `loadfile`, so a consumer that pauses before the attach
424    /// gets the deferred file loaded paused, exactly as if it had been
425    /// playing.
426    ///
427    /// The defer-or-load decision reads the **current** `vo` property,
428    /// so it tracks runtime `vo` changes (via
429    /// [`set_property`](Self::set_property)) and values picked up from a
430    /// config file — not just what the builder set. The same rule covers
431    /// the window after a [`detach_render`](Self::detach_render): with
432    /// `vo` still on the render API, sources queue again awaiting a
433    /// re-attach — a shell going render-less for good should switch `vo`
434    /// (e.g. to `null`) so loads run immediately. A deferred `loadfile`
435    /// that fails at attach time surfaces as
436    /// [`PlaybackEvent::Failed`] on the next
437    /// [`pump_events`](Self::pump_events), never as an `Err` from the
438    /// attach call.
439    pub fn load_when_ready(&self, source: &str) -> Result<()> {
440        if !self.expects_render() || self.has_render() {
441            return self.load(source);
442        }
443        *self.pending_load.lock() = Some(source.to_owned());
444        // An attach can slip in between `has_render()` above and the
445        // queueing; it would find the slot empty and never load. Re-check
446        // and drain — the `take` inside makes exactly one loader win if
447        // the attach also saw the source.
448        if self.has_render() {
449            return self.load_pending();
450        }
451        Ok(())
452    }
453
454    /// Whether frames route through the render API right now: the
455    /// current `vo` property includes `libmpv` (it accepts a
456    /// comma-separated fallback chain). Read live rather than snapshotted
457    /// at build — `vo` is runtime-settable through this crate's own
458    /// [`set_property`](Self::set_property), and a config file can set it
459    /// behind the builder's back. On a read failure, err toward a plain
460    /// load: the `loadfile` then surfaces the real error instead of the
461    /// source silently parking in the queue.
462    fn expects_render(&self) -> bool {
463        self.get_property::<String>("vo")
464            .is_ok_and(|vo| vo.split(',').any(|part| part.trim() == "libmpv"))
465    }
466
467    /// Escape hatch: any mpv command, args passed as an array (no quoting
468    /// needed).
469    ///
470    /// Commands that decide what plays next — `loadfile`, `loadlist`,
471    /// `stop`, `quit`, `quit-watch-later` — also discard a load queued by
472    /// [`load_when_ready`](Self::load_when_ready), same as the typed
473    /// transport methods: this is the only way to issue `loadfile` with
474    /// flags, and a superseded source must not resurface at attach time.
475    pub fn command(&self, name: &str, args: &[&str]) -> Result<()> {
476        let mut argv = Vec::with_capacity(args.len() + 1);
477        argv.push(name);
478        argv.extend_from_slice(args);
479        let supersedes_pending = matches!(
480            name,
481            "loadfile" | "loadlist" | "stop" | "quit" | "quit-watch-later"
482        );
483        // Transport commands run under the pending_load guard (clear +
484        // command as one step); everything else goes straight through.
485        let _transport = supersedes_pending.then(|| {
486            let mut pending = self.pending_load.lock();
487            *pending = None;
488            pending
489        });
490        self.mpv.command(&argv)?;
491        Ok(())
492    }
493
494    /// Set an mpv property. Accepts `bool` / `i64` / `f64` / `&str` /
495    /// `String` (anything [`Into<PropertyValue>`]).
496    pub fn set_property(&self, name: &str, value: impl Into<PropertyValue>) -> Result<()> {
497        match value.into() {
498            PropertyValue::Flag(v) => self.mpv.set_property(name, v)?,
499            PropertyValue::Int(v) => self.mpv.set_property(name, v)?,
500            PropertyValue::Double(v) => self.mpv.set_property(name, v)?,
501            PropertyValue::Str(v) => self.mpv.set_property(name, v)?,
502        }
503        Ok(())
504    }
505
506    /// Read an mpv property as `bool`, `i64`, `f64`, or `String`
507    /// ([`PropertyGet`] is sealed to those).
508    pub fn get_property<T: PropertyGet>(&self, name: &str) -> Result<T> {
509        T::get_property(&self.mpv, name)
510    }
511
512    /// Pause (`true`) or resume (`false`) playback.
513    pub fn set_paused(&self, paused: bool) -> Result<()> {
514        self.set_property("pause", paused)
515    }
516
517    /// Best-effort pause-state query; `false` when nothing is loaded yet
518    /// — use [`is_idle`](Self::is_idle) to tell "playing" apart from
519    /// "nothing to play".
520    pub fn is_paused(&self) -> bool {
521        self.get_property("pause").unwrap_or(false)
522    }
523
524    /// True when no file is loaded (`idle-active`) — distinguishes
525    /// "nothing to pause" from [`is_paused`](Self::is_paused) being
526    /// `false`.
527    pub fn is_idle(&self) -> bool {
528        self.get_property("idle-active").unwrap_or(true)
529    }
530
531    /// Stop playback and unload the current file. Surfaces as
532    /// [`PlaybackEvent::Ended`] with [`EndReason::Stop`]. Also discards a
533    /// load queued by [`load_when_ready`](Self::load_when_ready) — there
534    /// is nothing left to play.
535    pub fn stop(&self) -> Result<()> {
536        // `command` clears the pending deferred load (transport command).
537        self.command("stop", &[])
538    }
539
540    /// Volume in percent: 0–100 is normal range, above 100 amplifies (up
541    /// to mpv's `volume-max`).
542    pub fn set_volume(&self, percent: f64) -> Result<()> {
543        self.set_property("volume", percent)
544    }
545
546    /// Best-effort volume query in percent.
547    pub fn volume(&self) -> Option<f64> {
548        self.get_property("volume").ok()
549    }
550
551    /// Mute (`true`) or unmute (`false`) audio.
552    pub fn set_muted(&self, muted: bool) -> Result<()> {
553        self.set_property("mute", muted)
554    }
555
556    /// Best-effort mute-state query; `false` when nothing is loaded yet.
557    pub fn is_muted(&self) -> bool {
558        self.get_property("mute").unwrap_or(false)
559    }
560
561    /// Playback speed multiplier (`1.0` = normal).
562    pub fn set_speed(&self, speed: f64) -> Result<()> {
563        self.set_property("speed", speed)
564    }
565
566    /// Best-effort speed query.
567    pub fn speed(&self) -> Option<f64> {
568        self.get_property("speed").ok()
569    }
570
571    /// Current playback position in seconds, if a file is loaded.
572    pub fn position(&self) -> Option<f64> {
573        self.get_property("time-pos").ok()
574    }
575
576    /// Total duration in seconds. `None` while mpv is still parsing or for
577    /// unknown-duration streams.
578    pub fn duration(&self) -> Option<f64> {
579        self.get_property("duration").ok()
580    }
581
582    /// Seek to an absolute position in seconds.
583    pub fn seek_absolute(&self, secs: f64) -> Result<()> {
584        self.command("seek", &[&format!("{secs:.3}"), "absolute"])
585    }
586
587    /// Seek by a delta in seconds (negative seeks backward).
588    pub fn seek_relative(&self, secs: f64) -> Result<()> {
589        self.command("seek", &[&format!("{secs:.3}"), "relative"])
590    }
591
592    /// Observe a property for changes: matching
593    /// [`PlaybackEvent::PropertyChanged`] events arrive via
594    /// [`pump_events`](Self::pump_events), starting with one carrying the
595    /// current value (handy for initializing UI state). `format` picks
596    /// the delivered [`PropertyValue`] variant; mpv coerces where it can.
597    ///
598    /// Typical player set: `pause` (Flag), `time-pos`/`duration`
599    /// (Double), `paused-for-cache` (Flag), `dwidth`/`dheight` (Int).
600    pub fn observe(&self, name: &str, format: PropertyFormat) -> Result<ObserveId> {
601        let fmt = match format {
602            PropertyFormat::Flag => Format::Flag,
603            PropertyFormat::Int => Format::Int64,
604            PropertyFormat::Double => Format::Double,
605            PropertyFormat::Str => Format::String,
606        };
607        let id = self.next_observe_id.fetch_add(1, Ordering::Relaxed);
608        self.mpv.observe_property(id, name, fmt)?;
609        Ok(ObserveId(id))
610    }
611
612    /// Cancel one observation made with [`observe`](Self::observe).
613    pub fn unobserve(&self, id: ObserveId) -> Result<()> {
614        self.mpv.unobserve_property(id.0)?;
615        Ok(())
616    }
617
618    /// Register a callback fired whenever mpv queues new events — the
619    /// push alternative to polling [`pump_events`](Self::pump_events) on
620    /// a timer, and the only timely signal when no frames are flowing
621    /// (audio-only playback, a load failure while paused).
622    ///
623    /// The callback also fires **once synchronously during this call**
624    /// (so the construct → share → register ordering documented on the
625    /// attach methods applies here too), and mpv may additionally fire
626    /// it spuriously. Treat a wakeup as "check the queue", never "an
627    /// event arrived".
628    ///
629    /// Fires on arbitrary mpv-internal threads — possibly several at
630    /// once (hence `Sync`), possibly re-entrantly with other engine
631    /// calls: do no work and call no engine methods inside — signal your
632    /// main loop and pump from there (the same bridging pattern as the
633    /// render-update callback). Replaces any previously registered
634    /// wakeup callback.
635    pub fn set_wakeup_callback(&self, on_wakeup: impl Fn() + Send + Sync + 'static) {
636        // rsmpv owns the closure lifecycle: a replaced callback is freed
637        // once its last in-flight invocation finishes (possibly on an
638        // mpv-internal thread), and everything is unhooked safely during
639        // handle teardown. mpv only invokes the latest registration.
640        self.mpv.set_wakeup_callback(on_wakeup);
641    }
642
643    /// Drain pending mpv events into typed [`PlaybackEvent`]s. Call on a
644    /// timer or after the update callback; never blocks.
645    ///
646    /// Built on rsmpv's non-blocking `poll_event` (`&self`; internally
647    /// serialized against libmpv's one-waiter-per-handle rule). The
648    /// engine adds the `pump` lock on top so each drain is atomic —
649    /// concurrent pollers would otherwise split the stream, tearing
650    /// ordered sequences across callers' batches.
651    pub fn pump_events(&self) -> Vec<PlaybackEvent> {
652        let _guard = self.pump.lock();
653        // Engine-synthesized events first (a deferred load that failed at
654        // attach time) — they predate whatever mpv has queued now.
655        let mut out = std::mem::take(&mut *self.deferred_events.lock());
656        while let Some(ev) = self.mpv.poll_event() {
657            match ev {
658                Event::Shutdown => out.push(PlaybackEvent::Shutdown),
659                Event::FileLoaded => out.push(PlaybackEvent::Loaded),
660                // An errored end-of-file (bad format, load failure,
661                // missing/corrupt/empty data) surfaces as a typed
662                // `Failed` carrying mpv's error code — never as a quiet
663                // `Ended`.
664                Event::EndFile {
665                    reason: EndFileReason::Error,
666                    error,
667                    ..
668                } => {
669                    // Event errors always map onto a raw libmpv code
670                    // (rsmpv decodes them from one); `error` itself is
671                    // only absent if mpv violates its own contract of
672                    // setting a code on errored ends. Generic backstops
673                    // both impossibilities.
674                    let code = error
675                        .and_then(|e| e.raw_code())
676                        .unwrap_or(sys::MPV_ERROR_GENERIC);
677                    out.push(PlaybackEvent::Failed {
678                        code,
679                        message: describe_code(code),
680                    });
681                }
682                Event::EndFile { reason, .. } => out.push(PlaybackEvent::Ended {
683                    reason: end_reason(reason),
684                }),
685                Event::PlaybackRestart => out.push(PlaybackEvent::PlaybackRestart),
686                Event::PropertyChange {
687                    userdata,
688                    name,
689                    data,
690                } => {
691                    // A `None` here means the property became unavailable
692                    // (or arrived in a format this crate doesn't deliver)
693                    // — skipped rather than delivered as a bogus value,
694                    // and the drain keeps going.
695                    if let Some(value) = property_value(data) {
696                        out.push(PlaybackEvent::PropertyChanged {
697                            id: ObserveId(userdata),
698                            name,
699                            value,
700                        });
701                    }
702                }
703                _ => {}
704            }
705        }
706        out
707    }
708
709    /// Create the OpenGL render context. Call with the target GL context
710    /// current (e.g. GTK: in the GLArea `realize` handler).
711    ///
712    /// `get_proc_address` resolves GL symbols (on Linux, EGL 1.5's
713    /// `eglGetProcAddress` covers everything mpv asks for — beware
714    /// libepoxy on glvnd builds, which doesn't export core GL symbols as
715    /// plain `dlsym`-able functions and makes mpv report
716    /// `MPV_ERROR_UNSUPPORTED`). `on_update` fires on **mpv's render
717    /// thread** — and once synchronously during this call — to signal "a
718    /// new frame wants drawing"; forward it to your main loop and call
719    /// [`render_gl`](Self::render_gl) from your draw handler.
720    ///
721    /// The synchronous first call arrives *before* the context is stored:
722    /// a [`render_gl`](Self::render_gl) from inside it no-ops (harmlessly
723    /// — mpv re-signals). It runs on the caller's thread but outside the
724    /// engine's render lock, so calling back into render methods cannot
725    /// deadlock.
726    ///
727    /// `on_update` typically captures a `Weak` handle to your player
728    /// state — construct the `Engine`, wrap it in your `Arc`/shared
729    /// structure, *then* attach with the weak-capturing closure, then
730    /// load.
731    ///
732    /// A successful attach also issues any load queued by
733    /// [`load_when_ready`](Self::load_when_ready). `Err` still means "no
734    /// context was attached" — a deferred `loadfile` that fails here
735    /// leaves the context in place and surfaces as
736    /// [`PlaybackEvent::Failed`] on the next
737    /// [`pump_events`](Self::pump_events) (the wakeup callback fires).
738    ///
739    /// `options` fixes the shell's render-loop discipline at attach:
740    /// frame pacing ([`GlRenderOptions::block_for_target_time`]) and
741    /// mpv's advanced control ([`GlRenderOptions::advanced_control`],
742    /// which obligates [`render_update`](Self::render_update) after every
743    /// update callback). [`GlRenderOptions::default`] is mpv's stock
744    /// behavior.
745    ///
746    /// # Safety
747    /// GL-context currency is a dynamic, per-call rule the type system
748    /// cannot capture (rsmpv's OpenGL constructor is `unsafe` for the
749    /// same reason, and this crate forwards the obligation rather than
750    /// hiding it): the target GL context must be current on the calling
751    /// thread now, on every later [`render_gl`](Self::render_gl) or
752    /// [`render_update`](Self::render_update), and when the context is
753    /// freed — [`detach_render`](Self::detach_render) or the engine's
754    /// drop. Violating the rule is undefined behavior.
755    pub unsafe fn attach_gl_render(
756        &self,
757        get_proc_address: ProcAddressFn,
758        options: GlRenderOptions,
759        on_update: impl Fn() + Send + Sync + 'static,
760    ) -> Result<()> {
761        let _attaching = self.attach.lock();
762        if self.render.lock().is_some() {
763            return Err(Error::AlreadyAttached);
764        }
765        // Construct with the render lock *released*: registration fires
766        // `on_update` synchronously (via the relay), and holding the
767        // render lock across that call would deadlock an `on_update` that
768        // touches render methods. The attach lock keeps a second attacher
769        // out, so the slot check above stays authoritative. (An Arc clone
770        // goes in — never the engine's own reference — so a failed create
771        // can't drop the core.)
772        self.set_update_slot(Arc::new(on_update));
773        // SAFETY: GL-currency contract forwarded to the caller (above).
774        let created = unsafe {
775            GlRender::create(
776                self.mpv.clone(),
777                get_proc_address,
778                options,
779                self.update_relay(),
780            )
781        };
782        let render = match created {
783            Ok(r) => r,
784            Err(e) => {
785                // Failed attach: don't pin the shell closure's captures
786                // in a slot nothing will ever fire.
787                self.set_update_slot(Arc::new(|| {}));
788                return Err(e);
789            }
790        };
791        *self.render.lock() = Some(RenderBackend::Gl(render));
792        tracing::debug!("mpv GL render context attached");
793        self.drain_pending_load();
794        Ok(())
795    }
796
797    /// Create the software render context: frames arrive as RGBA bytes
798    /// via [`render_sw`](Self::render_sw), no GL anywhere — the
799    /// backend for shells that upload pixels themselves (or hand them to
800    /// a non-GL compositor). Unlike the GL backend there are no
801    /// context-current requirements, for rendering or teardown.
802    ///
803    /// `on_update` has the same contract as in
804    /// [`attach_gl_render`](Self::attach_gl_render): fires on mpv's
805    /// render thread plus once synchronously (outside the render lock,
806    /// before the context is stored), and typically captures a `Weak`
807    /// handle — construct, share, attach, then load.
808    ///
809    /// As with [`attach_gl_render`](Self::attach_gl_render): a successful
810    /// attach issues any [`load_when_ready`](Self::load_when_ready)
811    /// queue, `Err` still means "no context was attached", and a deferred
812    /// `loadfile` failing here surfaces as [`PlaybackEvent::Failed`] on
813    /// the next [`pump_events`](Self::pump_events) instead.
814    pub fn attach_sw_render(&self, on_update: impl Fn() + Send + Sync + 'static) -> Result<()> {
815        // Same locking shape as `attach_gl_render`, for the same reasons.
816        let _attaching = self.attach.lock();
817        if self.render.lock().is_some() {
818            return Err(Error::AlreadyAttached);
819        }
820        self.set_update_slot(Arc::new(on_update));
821        let render = match SwRender::create(self.mpv.clone(), self.update_relay()) {
822            Ok(r) => r,
823            Err(e) => {
824                self.set_update_slot(Arc::new(|| {}));
825                return Err(e);
826            }
827        };
828        *self.render.lock() = Some(RenderBackend::Sw(render));
829        tracing::debug!("mpv software render context attached");
830        self.drain_pending_load();
831        Ok(())
832    }
833
834    /// Issue the `loadfile` a [`load_when_ready`](Self::load_when_ready)
835    /// queued, now that a render context is stored. The guard is held
836    /// across the `loadfile` — take-then-load must be one transport step,
837    /// or a concurrent `stop`/`load` landing in between would be
838    /// overridden by the older, superseded source.
839    fn load_pending(&self) -> Result<()> {
840        let mut pending = self.pending_load.lock();
841        match pending.take() {
842            Some(source) => self.loadfile(&source),
843            None => Ok(()),
844        }
845    }
846
847    /// [`load_pending`](Self::load_pending) for the attach paths, where
848    /// an `Err` must keep meaning "no context was attached": the freshly
849    /// stored context stays either way, so a deferred-load failure is
850    /// rerouted onto the event stream as [`PlaybackEvent::Failed`] — the
851    /// channel an asynchronous load failure was headed for anyway — and
852    /// the wakeup callback is tickled so push-driven shells pump for it.
853    fn drain_pending_load(&self) {
854        let Err(err) = self.load_pending() else {
855            return;
856        };
857        tracing::warn!("deferred load failed at attach: {err}");
858        let code = match &err {
859            Error::Mpv(e) => e.raw_code().unwrap_or(sys::MPV_ERROR_GENERIC),
860            _ => sys::MPV_ERROR_GENERIC,
861        };
862        self.deferred_events.lock().push(PlaybackEvent::Failed {
863            code,
864            message: describe_code(code),
865        });
866        self.mpv.wakeup();
867    }
868
869    /// Drop the render context *now*. For the OpenGL backend, call with
870    /// the GL context still current (GTK: from the `unrealize` handler):
871    /// freeing without the right context current leaks mpv's GL objects
872    /// into whatever context is current — in GTK that painted artifacts
873    /// over the whole window. The software backend has no such
874    /// requirement; detach from any thread.
875    ///
876    /// After a detach, [`load_when_ready`](Self::load_when_ready) defers
877    /// again — `vo` still names the render API, so sources queue awaiting
878    /// a re-attach. A shell detaching *for good* (say, dropping to
879    /// audio-only) should also switch `vo` (e.g.
880    /// [`set_property`](Self::set_property)`("vo", "null")`); the
881    /// defer-or-load decision reads the live `vo`, so loads then run
882    /// immediately instead of parking.
883    pub fn detach_render(&self) {
884        if self.render.lock().take().is_some() {
885            // Release the shell's callback with the context: nothing can
886            // fire it anymore, and keeping it would pin its captures
887            // (typically shell window state) until the next attach.
888            self.set_update_slot(Arc::new(|| {}));
889            tracing::debug!("mpv render context detached");
890        }
891    }
892
893    /// Whether a render context (of either backend) is currently
894    /// attached. Delegates to [`attached_render`](Self::attached_render)
895    /// — one read of the slot, so the two can never disagree.
896    pub fn has_render(&self) -> bool {
897        self.attached_render().is_some()
898    }
899
900    /// Which render backend is attached, if any — the "which one"
901    /// companion to [`has_render`](Self::has_render), for shells that
902    /// route between per-backend code paths (say, GPU texture sampling
903    /// vs. RGBA upload) without having to track the attach outcome in
904    /// state of their own.
905    pub fn attached_render(&self) -> Option<RenderKind> {
906        self.render.lock().as_ref().map(RenderBackend::kind)
907    }
908
909    /// Store `cb` as the live render-update callback, dropping the
910    /// displaced closure *outside* the slot lock — its captures may carry
911    /// a `Drop` that calls back into the engine, which must not run under
912    /// any engine lock.
913    fn set_update_slot(&self, cb: UpdateCallback) {
914        let old = std::mem::replace(&mut *self.render_update_cb.lock(), cb);
915        drop(old);
916    }
917
918    /// The closure actually registered with rsmpv at attach: reads the
919    /// engine's callback slot on every fire, so
920    /// [`set_render_update_callback`](Self::set_render_update_callback)
921    /// can swap the target without re-registering through FFI. The `Arc`
922    /// is cloned out under a short lock and invoked with the lock
923    /// released — no engine lock is ever held across a callback
924    /// invocation.
925    fn update_relay(&self) -> impl Fn() + Send + Sync + 'static {
926        let slot = Arc::clone(&self.render_update_cb);
927        move || {
928            let cb = Arc::clone(&*slot.lock());
929            cb();
930        }
931    }
932
933    /// Replace the render-update callback registered at attach — the same
934    /// post-registration replaceability
935    /// [`set_wakeup_callback`](Self::set_wakeup_callback) has, for the
936    /// render seam. For shells that can only build their real closure
937    /// after the engine is shared: attach with a placeholder, wrap the
938    /// engine in your `Arc`/shared structure, then register the
939    /// weak-capturing closure here.
940    ///
941    /// The new callback takes over the attach-time contract: it fires on
942    /// mpv's render thread — and **once synchronously on the calling
943    /// thread, from inside this very call** (registration raises an
944    /// update immediately, so a frame signaled to the old callback isn't
945    /// lost). The synchronous fire runs outside every engine lock, same
946    /// as at attach — an engine call from inside it cannot deadlock. The
947    /// standing rule still applies to the mpv-thread fires, though: do no
948    /// work and call no engine methods inside — signal your main loop and
949    /// render/pump from there.
950    ///
951    /// The replaced closure is released with no engine lock held: on this
952    /// thread during this call when no invocation is in flight, otherwise
953    /// when its last in-flight invocation finishes — possibly on an
954    /// mpv-internal thread, so captures whose `Drop` calls into libmpv
955    /// (e.g. a last `Engine`-owning handle) don't belong in an update
956    /// callback.
957    ///
958    /// The registration is tied to the attached context:
959    /// [`detach_render`](Self::detach_render) releases it, and the next
960    /// attach starts from that attach's own `on_update`.
961    ///
962    /// Errors with [`Error::NotAttached`] when no render context is
963    /// attached — a callback that could never fire is a wiring bug,
964    /// surfaced loudly rather than silently dropped.
965    pub fn set_render_update_callback(
966        &self,
967        on_update: impl Fn() + Send + Sync + 'static,
968    ) -> Result<()> {
969        if !self.has_render() {
970            return Err(Error::NotAttached);
971        }
972        let new: UpdateCallback = Arc::new(on_update);
973        self.set_update_slot(Arc::clone(&new));
974        // The synchronous registration fire, with no engine lock held.
975        new();
976        Ok(())
977    }
978
979    /// Process pending render work after an update callback fired (never
980    /// call it from inside the callback itself — that's forbidden, like
981    /// any other engine call there). Returns `true` when a new frame
982    /// should be drawn. Optional under default options; **mandatory
983    /// promptly after every update callback** when the GL backend was
984    /// attached with [`GlRenderOptions::advanced_control`]. `false` when
985    /// no backend is attached. For the GL backend, the attach contract's
986    /// GL-currency rule covers this call too.
987    pub fn render_update(&self) -> bool {
988        self.render
989            .lock()
990            .as_mut()
991            .is_some_and(RenderBackend::update)
992    }
993
994    /// Draw the current frame into `fbo` (`0` = default framebuffer) with
995    /// the GL context current. No-op before
996    /// [`attach_gl_render`](Self::attach_gl_render); errors with
997    /// [`Error::RenderBackendMismatch`] if the software backend is
998    /// attached instead. `flip_y` flips the output for flipped-origin
999    /// targets (GTK's GLArea wants `true`). Whether this call blocks
1000    /// until the frame's target display time was fixed at attach
1001    /// ([`GlRenderOptions::block_for_target_time`]; the default blocks).
1002    pub fn render_gl(&self, fbo: i32, w: i32, h: i32, flip_y: bool) -> Result<()> {
1003        match self.render.lock().as_mut() {
1004            Some(RenderBackend::Gl(r)) => r.render(fbo, w, h, flip_y),
1005            Some(_) => Err(Error::RenderBackendMismatch),
1006            None => Ok(()),
1007        }
1008    }
1009
1010    /// Render the current frame as RGBA8 into `buf` (resized to
1011    /// `w * h * 4`; alpha always opaque). Callable from any thread.
1012    /// No-op before [`attach_sw_render`](Self::attach_sw_render) —
1013    /// `buf` is left untouched; errors with
1014    /// [`Error::RenderBackendMismatch`] if the OpenGL backend is
1015    /// attached instead.
1016    pub fn render_sw(&self, w: i32, h: i32, buf: &mut Vec<u8>) -> Result<()> {
1017        match self.render.lock().as_mut() {
1018            Some(RenderBackend::Sw(r)) => r.render(w, h, buf),
1019            Some(_) => Err(Error::RenderBackendMismatch),
1020            None => Ok(()),
1021        }
1022    }
1023}
1024
1025// No manual `Drop`: the render context co-owns the core through its own
1026// `Arc<Mpv>`, so it is structurally freed before the core terminates —
1027// field order can't break that anymore. If a GL target was attached,
1028// prefer an explicit `detach_render` with the GL context current before
1029// dropping; the implicit drop can't make that guarantee. (Wakeup- and
1030// update-callback teardown is rsmpv's: closures are released safely even
1031// against in-flight invocations, so mpv can never fire into freed memory.)