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