# MPV plugins in Rust

Bindings for libmpv client API that allow you to create plugins for MPV in Rust.

> ⚠️ **About this Fork:**
> This is a maintained fork of the original [TheCactusVert/mpv-client](https://github.com/TheCactusVert/mpv-client). 
> 
> **Key improvements in this fork:**
> * **Windows Support:** via MPV_CPLUGIN_DYNAMIC_SYM [linkage-to-libmpv](https://mpv.io/manual/stable/#linkage-to-libmpv).
> * **Android Support:** via MPV_CPLUGIN_DYNAMIC_SYM [linkage-to-libmpv](https://mpv.io/manual/stable/#linkage-to-libmpv).
> * **No LLVM/Clang required:** Uses **pregenerated bindings** by default, meaning you don't need `bindgen` or a local LLVM installation during `cargo build`.
> * **Ergonomic `#[mpv_client::main]` Macro:** Removes C-FFI boilerplate by wrapping your main function and automatically generating the underlying `mpv_open_cplugin` entry point.
> * **Built-in Configuration Parsing:** Includes a seamless `read_options()` helper with `serde` support to automatically merge and parse options from both configuration files (`~~/script-opts/`) and MPV command-line arguments.
> * **Logging:** The macro or ```mp.initialize_logging``` method automatically initializes a custom global `log` implementation (`MpvLogger`) that prints colored messages to stderr and seamlessly matches/synchronizes its timestamps with MPV's native `--log-file` timeline.

## Example

### Init plugin

Here is an example for your `Cargo.toml`:

```toml
[package]
name = "mpv-plugin"
version = "0.1.0"
edition = "2024"

[lib]
name = "mpv_plugin"
crate-type = ["cdylib"]

[dependencies]
mpv-client = { version = "3.0", package = "mpv-client-cross" }
```

And then the code `src/lib.rs`:

```rust
use mpv_client::{mpv_handle, Event, Handle};

#[no_mangle]
unsafe extern "C" fn mpv_open_cplugin(handle: *mut mpv_handle) -> i32 {
  let (mp, event_token) = unsafe { Handle::from_ptr(handle) };
  
  println!("Hello world from Rust plugin {}!", mp.name());
  
  loop {
    match mp.wait_event(&mut event_token, -1.) {
      Event::Shutdown => break,
      event => println!("Got event: {}", event),
    }
  }

  0
}
```

or

```toml
mpv-client = { version = "3.0", package = "mpv-client-cross", features = ["macros"] }
```

```rust
use mpv_client::{Event, Handle, EventQueueToken};

#[mpv_client::main]
fn main(mp: &Handle, mut event_token: EventQueueToken) -> i32 {
  log::info!("Hello world from Rust plugin {}!", mp.name());
  
  loop {
    match mp.wait_event(&mut event_token, -1.) {
      Event::Shutdown => break,
      event => log::info!("Got event: {}", event),
    }
  }

  0
}
```

⚠️ Important Warning:
#[mpv_client::main] macro automatically initializes global logger internally. If your code already sets up a third-party logger (for example, via env_logger::init(), log4rs, or similar crates), make sure to remove or disable its initialization. Attempting to initialize a global logger twice will result in a runtime error (application panic).

### Read config options

First, define your configuration structure using serde deserialization. It is highly recommended to use #[serde(default)] so the plugin can gracefully fall back to default values if options are missing from the configuration files.

```rust
use serde::Deserialize;
use mpv_client::{Event, Handle, EventQueueToken};

#[derive(Debug, Deserialize, Default)]
#[serde(default)]
pub struct PluginConfig {
    /// Enable or disable the plugin features
    pub enabled: bool,

    /// Maximum number of items to process/display
    pub max_items: u32,

    /// Optional API token or path to a custom asset
    pub auth_token: Option<String>,

    /// Select the visual layout style
    pub layout: LayoutMode,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum LayoutMode {
    #[default]
    Default,
    Compact,
    Expanded,
}
```

You can call read_options() directly on your Handle instance. The method automatically infers the plugin name and looks up settings from both configuration files and the command line.

```rust
#[mpv_client::main]
fn main(mp: &Handle, mut event_token: EventQueueToken) -> i32 {
    // Automatically reads options and falls back to defaults if not found
    let config: PluginConfig = mp.read_options();
    
    println!("Plugin initialized with config: {:#?}", config);
    
    loop {
        match mp.wait_event(&mut event_token, -1.) {
            Event::Shutdown => break,
            event => log::info!("Got event: {}", event),
        }
    }

    0
}
```

You can find more examples in [`C`](https://github.com/mpv-player/mpv-examples/tree/master/cplugins) and [`Rust`](https://github.com/TheCactusVert/mpv-sponsorblock).
