Skip to main content

#
jellytau_lib/player/
mpv_player.rs

    1//! [`MediaPlayer`] over libmpv.
    2//!
    3//! The point of difference from `MpvBackend` is [`MpvPlayer::open`]: the start
    4//! position is applied **at load time**, via mpv's own `start` option, instead
    5//! of being seeked to afterwards. `loadfile` is asynchronous, so a seek issued
    6//! after it targets a player that has nothing loaded, fails, and — under the old
    7//! contract — was discarded. That is DR-241, and it is why resume and transcoded
    8//! skip both played from zero.
    9//!
    10//! A seek arriving during [`Phase::Opening`] is held and applied when the file
    11//! loads, so no caller has to know where that window begins or ends.
    12//!
    13//! TRACES: UR-081, UR-040, UR-005 | DR-244
    14
    15#![allow(dead_code)] // Wired to PlayerController in DR-245.
    16
    17use std::sync::{Arc, Mutex};
    18use std::time::Duration;
    19
    20use libmpv::Mpv;
    21use log::{debug, info, warn};
    22
    23use super::backend::PlayerError;
    24use super::media_player::{
    25    duration_from_secs, Capabilities, MediaPlayer, OpenRequest, Phase, PlaybackSnapshot,
    26};
    27use crate::utils::lock::MutexSafe;
    28
    29/// State the event thread writes and the caller reads.
    30#[derive(Debug)]
    31struct Shared {
    32    phase: Phase,
    33    position: Duration,
    34    duration: Option<Duration>,
    35    seekable: bool,
    36    /// A seek that arrived while opening. Applied on `FileLoaded`.
    37    deferred_seek: Option<Duration>,
    38    /// Cleared by `close()`, so an open still in flight cannot come back to life
    39    /// and start playing after the caller has stopped it.
    40    open_generation: u64,
    41}
    42
    43impl Default for Shared {
    44    fn default() -> Self {
    45        Self {
    46            phase: Phase::Idle,
    47            position: Duration::ZERO,
    48            duration: None,
    49            seekable: false,
    50            deferred_seek: None,
    51            open_generation: 0,
    52        }
    53    }
    54}
    55
    56pub struct MpvPlayer {
    57    mpv: Arc<Mpv>,
    58    shared: Arc<Mutex<Shared>>,
    59    volume: f32,
    60    muted: bool,
    61    rate: f64,
    62    audio_track: Option<i32>,
    63    subtitle_track: Option<i32>,
    64}
    65
    66/// How the engine should talk to the machine.
    67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
    68pub enum Output {
    69    /// Real audio and video. What the app uses.
    70    Real,
    71    /// No audio device, no window. What conformance uses, so the suite can run
    72    /// on a headless runner without claiming the user's speakers.
    73    Null,
    74}
    75
    76impl MpvPlayer {
    77    pub fn new(output: Output) -> Result<Self, PlayerError> {
    78        // mpv refuses to start under a non-C LC_NUMERIC, and anything that has
    79        // initialised GTK before us will have set one.
    80        unsafe {
    81            let c = std::ffi::CString::new("C").unwrap();
    82            libc::setlocale(libc::LC_NUMERIC, c.as_ptr());
    83        }
    84
    85        let mpv = Mpv::new().map_err(|e| PlayerError {
    86            message: format!("mpv_create failed: {e:?}"),
    87        })?;
    88
    89        let set = |k: &str, v: &str| {
    90            if let Err(e) = mpv.set_property(k, v) {
    91                warn!("[MpvPlayer] could not set {k}={v}: {e:?}");
    92            }
    93        };
    94        match output {
    95            Output::Real => {
    96                set("vo", "libmpv");
    97            }
    98            Output::Null => {
    99                set("ao", "null");
    100                set("vo", "null");
    101            }
    102        }
    103        set("msg-level", "all=warn");
    104        // Survive a blip rather than ending the item on it.
    105        set(
    106            "stream-lavf-o",
    107            "reconnect=1,reconnect_streamed=1,reconnect_on_network_error=1,reconnect_delay_max=5",
    108        );
    109
    110        let player = Self {
    111            mpv: Arc::new(mpv),
    112            shared: Arc::new(Mutex::new(Shared::default())),
    113            volume: 1.0,
    114            muted: false,
    115            rate: 1.0,
    116            audio_track: None,
    117            subtitle_track: None,
    118        };
    119        player.spawn_events();
    120        Ok(player)
    121    }
    122
    123    fn spawn_events(&self) {
    124        let mpv = self.mpv.clone();
    125        let shared = self.shared.clone();
    126
    127        std::thread::spawn(move || {
    128            let mut ev = mpv.create_event_context();
    129            let _ = ev.disable_deprecated_events();
    130            // Every property matched below must be observed, or libmpv never
    131            // delivers it and the handler is unreachable (DR-239).
    132            for prop in ["pause", "eof-reached"] {
    133                if let Err(e) = ev.observe_property(prop, libmpv::Format::Flag, 0) {
    134                    warn!("[MpvPlayer] could not observe {prop}: {e:?}");
    135                }
    136            }
    137
    138            loop {
    139                match ev.wait_event(0.25) {
    140                    Some(Ok(libmpv::events::Event::FileLoaded)) => {
    141                        let deferred = {
    142                            let mut s = shared.lock_safe();
    143                            // Closed while opening: do not start.
    144                            if s.phase == Phase::Idle {
    145                                continue;
    146                            }
    147                            s.duration = mpv
    148                                .get_property::<f64>("duration")
    149                                .ok()
    150                                .and_then(duration_from_secs);
    151                            s.seekable = mpv.get_property::<bool>("seekable").unwrap_or(true);
    152                            s.phase = Phase::Playing;
    153                            s.deferred_seek.take()
    154                        };
    155                        if let Some(to) = deferred {
    156                            debug!("[MpvPlayer] applying deferred seek to {to:?}");
    157                            if let Err(e) = mpv.set_property("time-pos", to.as_secs_f64()) {
    158                                warn!("[MpvPlayer] deferred seek failed: {e:?}");
    159                            }
    160                        }
    161                    }
    162                    Some(Ok(libmpv::events::Event::PropertyChange { name: "pause", .. })) => {
    163                        if let Ok(paused) = mpv.get_property::<bool>("pause") {
    164                            let mut s = shared.lock_safe();
    165                            if s.phase.has_media() {
    166                                s.phase = if paused {
    167                                    Phase::Paused
    168                                } else {
    169                                    Phase::Playing
    170                                };
    171                            }
    172                        }
    173                    }
    174                    Some(Ok(libmpv::events::Event::EndFile(reason))) => {
    175                        let mut s = shared.lock_safe();
    176                        // 0 = EOF. Anything else is a stop, a quit or an error,
    177                        // and must not read as "the item finished".
    178                        s.phase = if reason == 0 {
    179                            Phase::Ended
    180                        } else {
    181                            Phase::Idle
    182                        };
    183                    }
    184                    Some(Ok(libmpv::events::Event::Shutdown)) => break,
    185                    _ => {}
    186                }
    187
    188                if let Ok(pos) = mpv.get_property::<f64>("time-pos") {
    189                    let mut s = shared.lock_safe();
    190                    if s.phase.has_media() && s.deferred_seek.is_none() {
    191                        s.position = Duration::from_secs_f64(pos.max(0.0));
    192                    }
    193                }
    194            }
    195        });
    196    }
    197}
    198
    199impl MediaPlayer for MpvPlayer {
    200    fn open(&mut self, req: OpenRequest) -> Result<(), PlayerError> {
    201        {
    202            let mut s = self.shared.lock_safe();
    203            *s = Shared {
    204                phase: Phase::Opening,
    205                open_generation: s.open_generation + 1,
    206                ..Shared::default()
    207            };
    208            // Report the requested position immediately, so a caller reading
    209            // back during the open sees where it asked to be rather than zero.
    210            s.position = req.start;
    211        }
    212
    213        // The whole point. `start` is applied by mpv as it opens the file, so
    214        // there is no window in which the position can be asked for and lost.
    215        let start = if req.start.is_zero() {
    216            "none".to_string()
    217        } else {
    218            format!("{:.3}", req.start.as_secs_f64())
    219        };
    220        self.mpv
    221            .set_property("start", start.as_str())
    222            .map_err(|e| PlayerError {
    223                message: format!("could not set start position: {e:?}"),
    224            })?;
    225        self.mpv
    226            .set_property("pause", !req.autoplay)
    227            .map_err(|e| PlayerError {
    228                message: format!("could not set pause: {e:?}"),
    229            })?;
    230
    231        info!("[MpvPlayer] open {} at {:?}", req.selection.url, req.start);
    232        self.mpv
    233            .command("loadfile", &[&req.selection.url, "replace"])
    234            .map_err(|e| PlayerError {
    235                message: format!("loadfile failed: {e:?}"),
    236            })?;
    237        Ok(())
    238    }
    239
    240    fn play(&mut self) -> Result<(), PlayerError> {
    241        self.mpv
    242            .set_property("pause", false)
    243            .map_err(|e| PlayerError {
    244                message: format!("play failed: {e:?}"),
    245            })?;
    246        let mut s = self.shared.lock_safe();
    247        if s.phase.has_media() && s.phase != Phase::Opening {
    248            s.phase = Phase::Playing;
    249        }
    250        Ok(())
    251    }
    252
    253    fn pause(&mut self) -> Result<(), PlayerError> {
    254        self.mpv
    255            .set_property("pause", true)
    256            .map_err(|e| PlayerError {
    257                message: format!("pause failed: {e:?}"),
    258            })?;
    259        let mut s = self.shared.lock_safe();
    260        if s.phase.has_media() && s.phase != Phase::Opening {
    261            s.phase = Phase::Paused;
    262        }
    263        Ok(())
    264    }
    265
    266    fn close(&mut self) -> Result<(), PlayerError> {
    267        // State first: an open still in flight checks this on FileLoaded and
    268        // must not proceed to play after the caller has stopped it.
    269        {
    270            let mut s = self.shared.lock_safe();
    271            *s = Shared {
    272                open_generation: s.open_generation,
    273                ..Shared::default()
    274            };
    275        }
    276        // Idempotent: stopping an already-stopped mpv is not an error worth
    277        // propagating, and callers legitimately close twice on teardown.
    278        if let Err(e) = self.mpv.command("stop", &[]) {
    279            debug!("[MpvPlayer] stop on an idle player: {e:?}");
    280        }
    281        Ok(())
    282    }
    283
    284    fn seek(&mut self, to: Duration) -> Result<(), PlayerError> {
    285        {
    286            let mut s = self.shared.lock_safe();
    287            match s.phase {
    288                // Held, not dropped. The caller cannot see this window.
    289                Phase::Opening => {
    290                    s.deferred_seek = Some(to);
    291                    s.position = to;
    292                    return Ok(());
    293                }
    294                Phase::Idle | Phase::Failed(_) => {
    295                    return Err(PlayerError {
    296                        message: "seek with nothing open".to_string(),
    297                    })
    298                }
    299                _ => s.position = to,
    300            }
    301        }
    302        self.mpv
    303            .set_property("time-pos", to.as_secs_f64())
    304            .map_err(|e| PlayerError {
    305                message: format!("seek failed: {e:?}"),
    306            })
    307    }
    308
    309    fn set_volume(&mut self, volume: f32) -> Result<(), PlayerError> {
    310        let clamped = volume.clamp(0.0, 1.0);
    311        self.volume = clamped;
    312        self.mpv
    313            .set_property("volume", (clamped as f64) * 100.0)
    314            .map_err(|e| PlayerError {
    315                message: format!("set_volume failed: {e:?}"),
    316            })
    317    }
    318
    319    fn set_muted(&mut self, muted: bool) -> Result<(), PlayerError> {
    320        self.muted = muted;
    321        self.mpv
    322            .set_property("mute", muted)
    323            .map_err(|e| PlayerError {
    324                message: format!("set_muted failed: {e:?}"),
    325            })
    326    }
    327
    328    fn set_rate(&mut self, rate: f64) -> Result<(), PlayerError> {
    329        self.rate = rate;
    330        self.mpv
    331            .set_property("speed", rate)
    332            .map_err(|e| PlayerError {
    333                message: format!("set_rate failed: {e:?}"),
    334            })
    335    }
    336
    337    fn select_audio_track(&mut self, index: Option<i32>) -> Result<(), PlayerError> {
    338        self.audio_track = index;
    339        let value = index.map(|i| i.to_string()).unwrap_or_else(|| "no".into());
    340        self.mpv
    341            .set_property("aid", value.as_str())
    342            .map_err(|e| PlayerError {
    343                message: format!("select_audio_track failed: {e:?}"),
    344            })
    345    }
    346
    347    fn select_subtitle_track(&mut self, index: Option<i32>) -> Result<(), PlayerError> {
    348        self.subtitle_track = index;
    349        let value = index.map(|i| i.to_string()).unwrap_or_else(|| "no".into());
    350        self.mpv
    351            .set_property("sid", value.as_str())
    352            .map_err(|e| PlayerError {
    353                message: format!("select_subtitle_track failed: {e:?}"),
    354            })
    355    }
    356
    357    fn snapshot(&self) -> PlaybackSnapshot {
    358        let s = self.shared.lock_safe();
    359        PlaybackSnapshot {
    360            phase: s.phase.clone(),
    361            position: s.position,
    362            duration: s.duration,
    363            seekable: s.seekable,
    364            volume: self.volume,
    365            muted: self.muted,
    366            rate: self.rate,
    367            audio_track: self.audio_track,
    368            subtitle_track: self.subtitle_track,
    369        }
    370    }
    371
    372    fn capabilities(&self) -> Capabilities {
    373        Capabilities {
    374            video: true,
    375            audio_settings: true,
    376            subtitle_switching: true,
    377            audio_track_switching: true,
    378            // mpv's HLS demuxer cannot make the server transcode from a new
    379            // offset, so a transcoded seek must re-open the stream.
    380            seeks_transcoded_in_place: false,
    381        }
    382    }
    383}