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::{GlRender, GlRenderOptions, ProcAddressFn, RenderBackend, SwRender};
9
10/// Force `LC_NUMERIC=C` exactly once before the first `mpv_create`. mpv
11/// refuses to work under a comma-decimal locale (its option/number parsing
12/// breaks), and toolkits like GTK call `setlocale()` from the environment
13/// during init — so this must run even when the host app never touches
14/// locale itself. Both parent projects carried this guard independently;
15/// it lives here now.
16///
17/// The category constant comes from `libc` because its value is
18/// platform-specific: `LC_NUMERIC` is 1 on glibc but 4 on BSD/macOS and
19/// Windows — a hardcoded 1 would silently set `LC_COLLATE` there.
20fn ensure_c_numeric_locale() {
21    static ONCE: Once = Once::new();
22    ONCE.call_once(|| unsafe {
23        libc::setlocale(libc::LC_NUMERIC, c"C".as_ptr());
24    });
25}
26
27/// Playback lifecycle notifications drained by [`Engine::pump_events`].
28///
29/// Non-exhaustive: match with a wildcard arm — new variants are additive,
30/// not breaking.
31#[derive(Debug, Clone, PartialEq)]
32#[non_exhaustive]
33pub enum PlaybackEvent {
34    /// A file finished loading and playback is starting.
35    Loaded,
36    /// Playback ended without error. Errored ends arrive as
37    /// [`Failed`](Self::Failed) instead.
38    Ended {
39        /// Why it ended — playlist logic usually advances on
40        /// [`EndReason::Eof`] and holds on [`EndReason::Stop`].
41        reason: EndReason,
42    },
43    /// Playback (re)started after a seek completed or a file began
44    /// playing: the displayed frame matches `time-pos` again. Snap
45    /// scrubber/position UI here, not while a seek is still in flight.
46    PlaybackRestart,
47    /// An observed property changed ([`Engine::observe`]); also fires
48    /// once with the current value right after observation starts.
49    PropertyChanged {
50        /// Which observation this notification belongs to — matches the
51        /// [`ObserveId`] returned by [`Engine::observe`], so two
52        /// observations of the same property stay distinguishable.
53        id: ObserveId,
54        /// The mpv property name, as passed to [`Engine::observe`].
55        name: String,
56        /// The new value, in the [`PropertyFormat`] the observation chose.
57        value: PropertyValue,
58    },
59    /// The mpv core is shutting down — e.g. a `quit` issued through
60    /// [`Engine::command`], or an input binding if input was enabled.
61    /// No further events follow; drop the [`Engine`] soon. (A file that
62    /// was playing also gets an [`Ended`](Self::Ended) with
63    /// [`EndReason::Quit`] first.)
64    Shutdown,
65    /// mpv aborted playback — unreadable, corrupt, empty, or an
66    /// unrecognized/unsupported format.
67    Failed {
68        /// Raw `client.h` `mpv_error` code (always negative). Match on
69        /// this to map failures to your own user-facing error copy.
70        code: i32,
71        /// Diagnostic text (mpv's `mpv_error_string` plus the code), not
72        /// user-facing copy.
73        message: String,
74    },
75}
76
77/// Why playback ended (mpv's `mpv_end_file_reason`).
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum EndReason {
80    /// Natural end of file.
81    Eof,
82    /// `stop` command or equivalent — user intent, don't auto-advance.
83    Stop,
84    /// The player is quitting.
85    Quit,
86    /// The entry redirected to another (e.g. a playlist file).
87    Redirect,
88    /// A reason this crate doesn't recognize; carries mpv's raw code.
89    Other(i32),
90}
91
92fn end_reason(reason: EndFileReason) -> EndReason {
93    match reason {
94        EndFileReason::Eof => EndReason::Eof,
95        EndFileReason::Stop => EndReason::Stop,
96        EndFileReason::Quit => EndReason::Quit,
97        EndFileReason::Redirect => EndReason::Redirect,
98        EndFileReason::Unknown(code) => EndReason::Other(code),
99        // `Error` is routed to `Failed` before this runs, and a future
100        // named reason (the enum is non_exhaustive) carries no raw code
101        // to forward — mpv's reason codes are non-negative, so -1 is
102        // recognizably out-of-band until this crate names the variant.
103        _ => EndReason::Other(-1),
104    }
105}
106
107/// Owned property value delivered by
108/// [`PropertyChanged`](PlaybackEvent::PropertyChanged).
109#[derive(Debug, Clone, PartialEq)]
110#[non_exhaustive]
111pub enum PropertyValue {
112    /// An mpv flag (`MPV_FORMAT_FLAG`).
113    Flag(bool),
114    /// A 64-bit integer (`MPV_FORMAT_INT64`).
115    Int(i64),
116    /// A double (`MPV_FORMAT_DOUBLE`).
117    Double(f64),
118    /// A string (`MPV_FORMAT_STRING`).
119    Str(String),
120}
121
122/// Map an event's decoded property payload onto this crate's owned value.
123/// `None` when the property is unavailable or the format is one this crate
124/// doesn't deliver (e.g. `Node`) — those notifications are skipped, not
125/// surfaced as bogus values.
126fn property_value(data: PropertyData) -> Option<PropertyValue> {
127    match data {
128        PropertyData::Flag(v) => Some(PropertyValue::Flag(v)),
129        PropertyData::Int64(v) => Some(PropertyValue::Int(v)),
130        PropertyData::Double(v) => Some(PropertyValue::Double(v)),
131        PropertyData::String(s) | PropertyData::OsdString(s) => Some(PropertyValue::Str(s)),
132        _ => None,
133    }
134}
135
136impl From<bool> for PropertyValue {
137    fn from(v: bool) -> Self {
138        Self::Flag(v)
139    }
140}
141impl From<i64> for PropertyValue {
142    fn from(v: i64) -> Self {
143        Self::Int(v)
144    }
145}
146impl From<i32> for PropertyValue {
147    fn from(v: i32) -> Self {
148        Self::Int(v.into())
149    }
150}
151impl From<u32> for PropertyValue {
152    fn from(v: u32) -> Self {
153        Self::Int(v.into())
154    }
155}
156impl From<f64> for PropertyValue {
157    fn from(v: f64) -> Self {
158        Self::Double(v)
159    }
160}
161impl From<&str> for PropertyValue {
162    fn from(v: &str) -> Self {
163        Self::Str(v.to_owned())
164    }
165}
166impl From<String> for PropertyValue {
167    fn from(v: String) -> Self {
168        Self::Str(v)
169    }
170}
171
172mod sealed {
173    use crate::error::Result;
174
175    pub trait PropertyGetImpl: Sized {
176        fn get_property(mpv: &rsmpv::Mpv, name: &str) -> Result<Self>;
177    }
178
179    macro_rules! impl_property_get {
180        ($($t:ty),*) => {$(
181            impl PropertyGetImpl for $t {
182                fn get_property(mpv: &rsmpv::Mpv, name: &str) -> Result<Self> {
183                    Ok(mpv.get_property(name)?)
184                }
185            }
186        )*};
187    }
188    impl_property_get!(bool, i64, f64, String);
189}
190
191/// Types a property can be read as: `bool`, `i64`, `f64`, `String`.
192/// Sealed to the formats mpv's property API speaks — the binding's own
193/// conversion traits are deliberately not part of this crate's API, so
194/// a binding major bump can't be a breaking change here.
195pub trait PropertyGet: sealed::PropertyGetImpl {}
196impl PropertyGet for bool {}
197impl PropertyGet for i64 {}
198impl PropertyGet for f64 {}
199impl PropertyGet for String {}
200
201/// Wire format for [`Engine::observe`] — picks which [`PropertyValue`]
202/// variant change notifications carry (mpv coerces where it can).
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub enum PropertyFormat {
205    /// Deliver as [`PropertyValue::Flag`].
206    Flag,
207    /// Deliver as [`PropertyValue::Int`].
208    Int,
209    /// Deliver as [`PropertyValue::Double`].
210    Double,
211    /// Deliver as [`PropertyValue::Str`].
212    Str,
213}
214
215/// Handle returned by [`Engine::observe`]: tags that observation's
216/// [`PropertyChanged`](PlaybackEvent::PropertyChanged) events and cancels
217/// it via [`Engine::unobserve`].
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub struct ObserveId(u64);
220
221/// Configures and creates an [`Engine`]. Properties set here are applied
222/// before `mpv_initialize`, which some options require.
223pub struct EngineBuilder {
224    props: Vec<(String, String)>,
225}
226
227impl EngineBuilder {
228    /// Set an mpv property/option before initialization.
229    pub fn property(mut self, name: &str, value: &str) -> Self {
230        self.props.push((name.into(), value.into()));
231        self
232    }
233
234    /// Create the engine: forces `LC_NUMERIC=C`, applies the queued
235    /// properties, and runs `mpv_initialize`.
236    pub fn build(self) -> Result<Engine> {
237        ensure_c_numeric_locale();
238        let mut builder = Mpv::builder()?;
239        for (name, value) in &self.props {
240            builder = builder.set_property(name, value.as_str())?;
241        }
242        let mpv = Arc::new(builder.build()?);
243        Ok(Engine {
244            render: Mutex::new(None),
245            attach: Mutex::new(()),
246            pump: Mutex::new(()),
247            next_observe_id: AtomicU64::new(1),
248            mpv,
249        })
250    }
251}
252
253/// One embedded mpv player core.
254///
255/// Toolkit-agnostic by construction: no main-loop integration, no widget,
256/// no GL context of its own. A shell attaches a render target with
257/// [`attach_gl_render`](Engine::attach_gl_render), forwards the update
258/// callback to its own main loop, and drains
259/// [`pump_events`](Engine::pump_events) on whatever cadence suits it.
260pub struct Engine {
261    /// Live render context (either backend), if any. Freed-before-
262    /// terminate ordering is structural: the context co-owns the core
263    /// through its own `Arc<Mpv>`, so the core cannot terminate under a
264    /// live context regardless of field order.
265    render: Mutex<Option<RenderBackend>>,
266    /// Serializes attach calls: concurrent `mpv_render_context_create`
267    /// on one handle violates render.h's threading rules, and slot
268    /// re-checks alone can't prevent the double-create. Deliberately not
269    /// the `render` lock — the synchronous `on_update` during create
270    /// must stay free to touch render methods.
271    attach: Mutex<()>,
272    /// Keeps each [`pump_events`](Engine::pump_events) drain atomic.
273    /// rsmpv's `poll_event` is safe to call concurrently, but concurrent
274    /// pollers *split* the stream (each event goes to exactly one
275    /// caller) — two racing pumps would tear ordered sequences like
276    /// `Loaded` → `Ended` across their result batches.
277    pump: Mutex<()>,
278    /// Userdata ids handed to `mpv_observe_property`; each observation
279    /// gets a fresh one so [`unobserve`](Engine::unobserve) is precise.
280    next_observe_id: AtomicU64,
281    /// Shared with any live render context, which holds its own clone.
282    mpv: Arc<Mpv>,
283}
284
285// `Engine: Send + Sync` is auto-derived: rsmpv marks `Mpv` Send + Sync
286// (libmpv is thread-safe per client.h; the one caveat, single-waiter
287// `mpv_wait_event`, is upheld inside rsmpv's `poll_event`), the render
288// contexts are `Send` behind a `Sync` mutex, and the remaining fields
289// are sync primitives. Do not add manual `unsafe impl`s — they'd
290// silence the compiler if a future field is genuinely `!Send`.
291
292impl Engine {
293    /// Neutral builder with no properties preset — for consumers whose
294    /// configuration doesn't start from [`video`](Self::video) or
295    /// [`headless`](Self::headless). (The presets can also be overridden:
296    /// properties apply in call order.)
297    pub fn builder() -> EngineBuilder {
298        EngineBuilder { props: vec![] }
299    }
300
301    /// Builder preset for video playback via the libmpv render API.
302    /// Frames appear only after [`attach_gl_render`](Self::attach_gl_render);
303    /// see that method's note on load ordering.
304    pub fn video() -> EngineBuilder {
305        Self::builder()
306            .property("vo", "libmpv")
307            .property("hwdec", "auto-safe")
308            .property("keep-open", "always")
309    }
310
311    /// Builder preset for audio-only / headless use (`vo=null`): playback
312    /// starts as soon as [`load`](Self::load) runs, no render target needed.
313    /// Also what the test suite uses — no display required.
314    pub fn headless() -> EngineBuilder {
315        Self::builder()
316            .property("vo", "null")
317            .property("keep-open", "always")
318    }
319
320    /// Load a file path or URL and start playback.
321    ///
322    /// For video engines, prefer [`load_paused`](Self::load_paused) until
323    /// the shell's surface is mapped: `loadfile` before a render context
324    /// exists leaves mpv with nowhere to send frames (audio plays, video
325    /// stays black), and demuxing before the window shows wastes work.
326    pub fn load(&self, source: &str) -> Result<()> {
327        // rsmpv passes args as an array (`mpv_command`), so paths with
328        // spaces/quotes need no escaping here. Do not "simplify" this
329        // into a formatted command string — that reintroduces the quoting
330        // bug this crate's regression test pins.
331        self.mpv.command(&["loadfile", source])?;
332        Ok(())
333    }
334
335    /// [`load`](Self::load), but paused: pause is set *before* `loadfile`
336    /// so demuxing/audio don't start before the shell is ready. Call
337    /// [`set_paused`](Self::set_paused)`(false)` on your window-ready
338    /// signal. (Setting `pause` at init time instead has a tendency to
339    /// hang — this runtime-property ordering is the reliable variant.)
340    pub fn load_paused(&self, source: &str) -> Result<()> {
341        self.set_paused(true)?;
342        self.load(source)
343    }
344
345    /// Escape hatch: any mpv command, args passed as an array (no quoting
346    /// needed).
347    pub fn command(&self, name: &str, args: &[&str]) -> Result<()> {
348        let mut argv = Vec::with_capacity(args.len() + 1);
349        argv.push(name);
350        argv.extend_from_slice(args);
351        self.mpv.command(&argv)?;
352        Ok(())
353    }
354
355    /// Set an mpv property. Accepts `bool` / `i64` / `f64` / `&str` /
356    /// `String` (anything [`Into<PropertyValue>`]).
357    pub fn set_property(&self, name: &str, value: impl Into<PropertyValue>) -> Result<()> {
358        match value.into() {
359            PropertyValue::Flag(v) => self.mpv.set_property(name, v)?,
360            PropertyValue::Int(v) => self.mpv.set_property(name, v)?,
361            PropertyValue::Double(v) => self.mpv.set_property(name, v)?,
362            PropertyValue::Str(v) => self.mpv.set_property(name, v)?,
363        }
364        Ok(())
365    }
366
367    /// Read an mpv property as `bool`, `i64`, `f64`, or `String`
368    /// ([`PropertyGet`] is sealed to those).
369    pub fn get_property<T: PropertyGet>(&self, name: &str) -> Result<T> {
370        T::get_property(&self.mpv, name)
371    }
372
373    /// Pause (`true`) or resume (`false`) playback.
374    pub fn set_paused(&self, paused: bool) -> Result<()> {
375        self.set_property("pause", paused)
376    }
377
378    /// Best-effort pause-state query; `false` when nothing is loaded yet
379    /// — use [`is_idle`](Self::is_idle) to tell "playing" apart from
380    /// "nothing to play".
381    pub fn is_paused(&self) -> bool {
382        self.get_property("pause").unwrap_or(false)
383    }
384
385    /// True when no file is loaded (`idle-active`) — distinguishes
386    /// "nothing to pause" from [`is_paused`](Self::is_paused) being
387    /// `false`.
388    pub fn is_idle(&self) -> bool {
389        self.get_property("idle-active").unwrap_or(true)
390    }
391
392    /// Stop playback and unload the current file. Surfaces as
393    /// [`PlaybackEvent::Ended`] with [`EndReason::Stop`].
394    pub fn stop(&self) -> Result<()> {
395        self.command("stop", &[])
396    }
397
398    /// Volume in percent: 0–100 is normal range, above 100 amplifies (up
399    /// to mpv's `volume-max`).
400    pub fn set_volume(&self, percent: f64) -> Result<()> {
401        self.set_property("volume", percent)
402    }
403
404    /// Best-effort volume query in percent.
405    pub fn volume(&self) -> Option<f64> {
406        self.get_property("volume").ok()
407    }
408
409    /// Mute (`true`) or unmute (`false`) audio.
410    pub fn set_muted(&self, muted: bool) -> Result<()> {
411        self.set_property("mute", muted)
412    }
413
414    /// Best-effort mute-state query; `false` when nothing is loaded yet.
415    pub fn is_muted(&self) -> bool {
416        self.get_property("mute").unwrap_or(false)
417    }
418
419    /// Playback speed multiplier (`1.0` = normal).
420    pub fn set_speed(&self, speed: f64) -> Result<()> {
421        self.set_property("speed", speed)
422    }
423
424    /// Best-effort speed query.
425    pub fn speed(&self) -> Option<f64> {
426        self.get_property("speed").ok()
427    }
428
429    /// Current playback position in seconds, if a file is loaded.
430    pub fn position(&self) -> Option<f64> {
431        self.get_property("time-pos").ok()
432    }
433
434    /// Total duration in seconds. `None` while mpv is still parsing or for
435    /// unknown-duration streams.
436    pub fn duration(&self) -> Option<f64> {
437        self.get_property("duration").ok()
438    }
439
440    /// Seek to an absolute position in seconds.
441    pub fn seek_absolute(&self, secs: f64) -> Result<()> {
442        self.command("seek", &[&format!("{secs:.3}"), "absolute"])
443    }
444
445    /// Seek by a delta in seconds (negative seeks backward).
446    pub fn seek_relative(&self, secs: f64) -> Result<()> {
447        self.command("seek", &[&format!("{secs:.3}"), "relative"])
448    }
449
450    /// Observe a property for changes: matching
451    /// [`PlaybackEvent::PropertyChanged`] events arrive via
452    /// [`pump_events`](Self::pump_events), starting with one carrying the
453    /// current value (handy for initializing UI state). `format` picks
454    /// the delivered [`PropertyValue`] variant; mpv coerces where it can.
455    ///
456    /// Typical player set: `pause` (Flag), `time-pos`/`duration`
457    /// (Double), `paused-for-cache` (Flag), `dwidth`/`dheight` (Int).
458    pub fn observe(&self, name: &str, format: PropertyFormat) -> Result<ObserveId> {
459        let fmt = match format {
460            PropertyFormat::Flag => Format::Flag,
461            PropertyFormat::Int => Format::Int64,
462            PropertyFormat::Double => Format::Double,
463            PropertyFormat::Str => Format::String,
464        };
465        let id = self.next_observe_id.fetch_add(1, Ordering::Relaxed);
466        self.mpv.observe_property(id, name, fmt)?;
467        Ok(ObserveId(id))
468    }
469
470    /// Cancel one observation made with [`observe`](Self::observe).
471    pub fn unobserve(&self, id: ObserveId) -> Result<()> {
472        self.mpv.unobserve_property(id.0)?;
473        Ok(())
474    }
475
476    /// Register a callback fired whenever mpv queues new events — the
477    /// push alternative to polling [`pump_events`](Self::pump_events) on
478    /// a timer, and the only timely signal when no frames are flowing
479    /// (audio-only playback, a load failure while paused).
480    ///
481    /// The callback also fires **once synchronously during this call**
482    /// (so the construct → share → register ordering documented on the
483    /// attach methods applies here too), and mpv may additionally fire
484    /// it spuriously. Treat a wakeup as "check the queue", never "an
485    /// event arrived".
486    ///
487    /// Fires on arbitrary mpv-internal threads — possibly several at
488    /// once (hence `Sync`), possibly re-entrantly with other engine
489    /// calls: do no work and call no engine methods inside — signal your
490    /// main loop and pump from there (the same bridging pattern as the
491    /// render-update callback). Replaces any previously registered
492    /// wakeup callback.
493    pub fn set_wakeup_callback(&self, on_wakeup: impl Fn() + Send + Sync + 'static) {
494        // rsmpv owns the closure lifecycle: a replaced callback is freed
495        // once its last in-flight invocation finishes (possibly on an
496        // mpv-internal thread), and everything is unhooked safely during
497        // handle teardown. mpv only invokes the latest registration.
498        self.mpv.set_wakeup_callback(on_wakeup);
499    }
500
501    /// Drain pending mpv events into typed [`PlaybackEvent`]s. Call on a
502    /// timer or after the update callback; never blocks.
503    ///
504    /// Built on rsmpv's non-blocking `poll_event` (`&self`; internally
505    /// serialized against libmpv's one-waiter-per-handle rule). The
506    /// engine adds the `pump` lock on top so each drain is atomic —
507    /// concurrent pollers would otherwise split the stream, tearing
508    /// ordered sequences across callers' batches.
509    pub fn pump_events(&self) -> Vec<PlaybackEvent> {
510        let _guard = self.pump.lock();
511        let mut out = Vec::new();
512        while let Some(ev) = self.mpv.poll_event() {
513            match ev {
514                Event::Shutdown => out.push(PlaybackEvent::Shutdown),
515                Event::FileLoaded => out.push(PlaybackEvent::Loaded),
516                // An errored end-of-file (bad format, load failure,
517                // missing/corrupt/empty data) surfaces as a typed
518                // `Failed` carrying mpv's error code — never as a quiet
519                // `Ended`.
520                Event::EndFile {
521                    reason: EndFileReason::Error,
522                    error,
523                    ..
524                } => {
525                    // Event errors always map onto a raw libmpv code
526                    // (rsmpv decodes them from one); `error` itself is
527                    // only absent if mpv violates its own contract of
528                    // setting a code on errored ends. Generic backstops
529                    // both impossibilities.
530                    let code = error
531                        .and_then(|e| e.raw_code())
532                        .unwrap_or(sys::MPV_ERROR_GENERIC);
533                    out.push(PlaybackEvent::Failed {
534                        code,
535                        message: describe_code(code),
536                    });
537                }
538                Event::EndFile { reason, .. } => out.push(PlaybackEvent::Ended {
539                    reason: end_reason(reason),
540                }),
541                Event::PlaybackRestart => out.push(PlaybackEvent::PlaybackRestart),
542                Event::PropertyChange {
543                    userdata,
544                    name,
545                    data,
546                } => {
547                    // A `None` here means the property became unavailable
548                    // (or arrived in a format this crate doesn't deliver)
549                    // — skipped rather than delivered as a bogus value,
550                    // and the drain keeps going.
551                    if let Some(value) = property_value(data) {
552                        out.push(PlaybackEvent::PropertyChanged {
553                            id: ObserveId(userdata),
554                            name,
555                            value,
556                        });
557                    }
558                }
559                _ => {}
560            }
561        }
562        out
563    }
564
565    /// Create the OpenGL render context. Call with the target GL context
566    /// current (e.g. GTK: in the GLArea `realize` handler).
567    ///
568    /// `get_proc_address` resolves GL symbols (on Linux, EGL 1.5's
569    /// `eglGetProcAddress` covers everything mpv asks for — beware
570    /// libepoxy on glvnd builds, which doesn't export core GL symbols as
571    /// plain `dlsym`-able functions and makes mpv report
572    /// `MPV_ERROR_UNSUPPORTED`). `on_update` fires on **mpv's render
573    /// thread** — and once synchronously during this call — to signal "a
574    /// new frame wants drawing"; forward it to your main loop and call
575    /// [`render_gl`](Self::render_gl) from your draw handler.
576    ///
577    /// The synchronous first call arrives *before* the context is stored:
578    /// a [`render_gl`](Self::render_gl) from inside it no-ops (harmlessly
579    /// — mpv re-signals). It runs on the caller's thread but outside the
580    /// engine's render lock, so calling back into render methods cannot
581    /// deadlock.
582    ///
583    /// `on_update` typically captures a `Weak` handle to your player
584    /// state — construct the `Engine`, wrap it in your `Arc`/shared
585    /// structure, *then* attach with the weak-capturing closure, then
586    /// load.
587    ///
588    /// `options` fixes the shell's render-loop discipline at attach:
589    /// frame pacing ([`GlRenderOptions::block_for_target_time`]) and
590    /// mpv's advanced control ([`GlRenderOptions::advanced_control`],
591    /// which obligates [`render_update`](Self::render_update) after every
592    /// update callback). [`GlRenderOptions::default`] is mpv's stock
593    /// behavior.
594    ///
595    /// # Safety
596    /// GL-context currency is a dynamic, per-call rule the type system
597    /// cannot capture (rsmpv's OpenGL constructor is `unsafe` for the
598    /// same reason, and this crate forwards the obligation rather than
599    /// hiding it): the target GL context must be current on the calling
600    /// thread now, on every later [`render_gl`](Self::render_gl) or
601    /// [`render_update`](Self::render_update), and when the context is
602    /// freed — [`detach_render`](Self::detach_render) or the engine's
603    /// drop. Violating the rule is undefined behavior.
604    pub unsafe fn attach_gl_render(
605        &self,
606        get_proc_address: ProcAddressFn,
607        options: GlRenderOptions,
608        on_update: impl Fn() + Send + Sync + 'static,
609    ) -> Result<()> {
610        let _attaching = self.attach.lock();
611        if self.render.lock().is_some() {
612            return Err(Error::AlreadyAttached);
613        }
614        // Construct with the render lock *released*: registration fires
615        // `on_update` synchronously, and holding the render lock across
616        // that call would deadlock an `on_update` that touches render
617        // methods. The attach lock keeps a second attacher out, so the
618        // slot check above stays authoritative. (An Arc clone goes in —
619        // never the engine's own reference — so a failed create can't
620        // drop the core.)
621        // SAFETY: GL-currency contract forwarded to the caller (above).
622        let render =
623            unsafe { GlRender::create(self.mpv.clone(), get_proc_address, options, on_update)? };
624        *self.render.lock() = Some(RenderBackend::Gl(render));
625        tracing::debug!("mpv GL render context attached");
626        Ok(())
627    }
628
629    /// Create the software render context: frames arrive as RGBA bytes
630    /// via [`render_sw`](Self::render_sw), no GL anywhere — the
631    /// backend for shells that upload pixels themselves (or hand them to
632    /// a non-GL compositor). Unlike the GL backend there are no
633    /// context-current requirements, for rendering or teardown.
634    ///
635    /// `on_update` has the same contract as in
636    /// [`attach_gl_render`](Self::attach_gl_render): fires on mpv's
637    /// render thread plus once synchronously (outside the render lock,
638    /// before the context is stored), and typically captures a `Weak`
639    /// handle — construct, share, attach, then load.
640    pub fn attach_sw_render(&self, on_update: impl Fn() + Send + Sync + 'static) -> Result<()> {
641        // Same locking shape as `attach_gl_render`, for the same reasons.
642        let _attaching = self.attach.lock();
643        if self.render.lock().is_some() {
644            return Err(Error::AlreadyAttached);
645        }
646        let render = SwRender::create(self.mpv.clone(), on_update)?;
647        *self.render.lock() = Some(RenderBackend::Sw(render));
648        tracing::debug!("mpv software render context attached");
649        Ok(())
650    }
651
652    /// Drop the render context *now*. For the OpenGL backend, call with
653    /// the GL context still current (GTK: from the `unrealize` handler):
654    /// freeing without the right context current leaks mpv's GL objects
655    /// into whatever context is current — in GTK that painted artifacts
656    /// over the whole window. The software backend has no such
657    /// requirement; detach from any thread.
658    pub fn detach_render(&self) {
659        if self.render.lock().take().is_some() {
660            tracing::debug!("mpv render context detached");
661        }
662    }
663
664    /// Whether a render context (of either backend) is currently attached.
665    pub fn has_render(&self) -> bool {
666        self.render.lock().is_some()
667    }
668
669    /// Process pending render work after an update callback fired (never
670    /// call it from inside the callback itself — that's forbidden, like
671    /// any other engine call there). Returns `true` when a new frame
672    /// should be drawn. Optional under default options; **mandatory
673    /// promptly after every update callback** when the GL backend was
674    /// attached with [`GlRenderOptions::advanced_control`]. `false` when
675    /// no backend is attached. For the GL backend, the attach contract's
676    /// GL-currency rule covers this call too.
677    pub fn render_update(&self) -> bool {
678        self.render
679            .lock()
680            .as_mut()
681            .is_some_and(RenderBackend::update)
682    }
683
684    /// Draw the current frame into `fbo` (`0` = default framebuffer) with
685    /// the GL context current. No-op before
686    /// [`attach_gl_render`](Self::attach_gl_render); errors with
687    /// [`Error::RenderBackendMismatch`] if the software backend is
688    /// attached instead. `flip_y` flips the output for flipped-origin
689    /// targets (GTK's GLArea wants `true`). Whether this call blocks
690    /// until the frame's target display time was fixed at attach
691    /// ([`GlRenderOptions::block_for_target_time`]; the default blocks).
692    pub fn render_gl(&self, fbo: i32, w: i32, h: i32, flip_y: bool) -> Result<()> {
693        match self.render.lock().as_mut() {
694            Some(RenderBackend::Gl(r)) => r.render(fbo, w, h, flip_y),
695            Some(_) => Err(Error::RenderBackendMismatch),
696            None => Ok(()),
697        }
698    }
699
700    /// Render the current frame as RGBA8 into `buf` (resized to
701    /// `w * h * 4`; alpha always opaque). Callable from any thread.
702    /// No-op before [`attach_sw_render`](Self::attach_sw_render) —
703    /// `buf` is left untouched; errors with
704    /// [`Error::RenderBackendMismatch`] if the OpenGL backend is
705    /// attached instead.
706    pub fn render_sw(&self, w: i32, h: i32, buf: &mut Vec<u8>) -> Result<()> {
707        match self.render.lock().as_mut() {
708            Some(RenderBackend::Sw(r)) => r.render(w, h, buf),
709            Some(_) => Err(Error::RenderBackendMismatch),
710            None => Ok(()),
711        }
712    }
713}
714
715// No manual `Drop`: the render context co-owns the core through its own
716// `Arc<Mpv>`, so it is structurally freed before the core terminates —
717// field order can't break that anymore. If a GL target was attached,
718// prefer an explicit `detach_render` with the GL context current before
719// dropping; the implicit drop can't make that guarantee. (Wakeup- and
720// update-callback teardown is rsmpv's: closures are released safely even
721// against in-flight invocations, so mpv can never fire into freed memory.)