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.)