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