tauri-plugin-mpv 0.5.2

A Tauri plugin for embedding the mpv player in your app by controlling its process via JSON IPC.
Documentation
use log::{debug, error, info, trace, warn};
use raw_window_handle::HasWindowHandle;
use std::collections::VecDeque;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use tauri::{AppHandle, Manager, Runtime};

use crate::events::{self};
use crate::ipc::get_ipc_pipe;
use crate::utils::get_wid;
use crate::{MpvConfig, MpvExt, MpvInstance};

pub fn init_mpv_process<R: Runtime>(
    app: &AppHandle<R>,
    mpv_config: MpvConfig,
    window_label: &str,
) -> crate::Result<()> {
    let ipc_pipe = get_ipc_pipe(window_label);
    let ipc_timeout = Duration::from_millis(mpv_config.ipc_timeout_ms);

    let mut instances_lock = app.mpv().instances.lock().unwrap();
    if let Some(instance) = instances_lock.get_mut(window_label) {
        if instance.process.try_wait().unwrap_or(None).is_none() {
            match wait_for_ipc_server(&ipc_pipe, ipc_timeout, window_label) {
                Ok(_) => {
                    info!(
                        "mpv process (PID: {}) for window '{}' is still running. Skipping initialization.",
                        instance.process.id(),
                        window_label
                    );
                    return Ok(());
                }
                Err(e) => {
                    warn!(
                        "Failed to connect to IPC for window '{}': {}, killing and restarting...",
                        window_label, e
                    );
                    instance.process.kill().unwrap();
                }
            }
        }
    }

    info!("Initializing mpv for window '{}'...", window_label);

    let audio_only = mpv_config
        .args
        .iter()
        .any(|arg| arg == "--no-video" || arg == "--video=no" || arg == "--vid=no");

    let wid_arg = mpv_config
        .args
        .iter()
        .find_map(|arg| arg.strip_prefix("--wid="));

    let parsed_wid: Option<i64> = wid_arg.and_then(|wid_str| match wid_str.parse() {
        Ok(wid) => Some(wid),
        Err(_) => {
            warn!(
                "Failed to parse provided wid '{}'. Falling back to window handle.",
                wid_str
            );
            None
        }
    });

    let wid: Option<i64> = if let Some(parsed_wid) = parsed_wid {
        Some(parsed_wid)
    } else if audio_only {
        info!(
            "Audio-only mode detected for window '{}'. Skipping window embedding.",
            window_label
        );
        None
    } else {
        let wid_result = (|| -> crate::Result<i64> {
            let window = app
                .get_webview_window(window_label)
                .ok_or_else(|| crate::Error::WindowNotFound(window_label.to_string()))?;
            get_wid(window.window_handle()?.as_raw())
        })();

        match wid_result {
            Ok(w) => Some(w),
            Err(e) => {
                error!(
                    "Failed to get wid for window '{}': {}. Skipping window embedding.",
                    window_label, e
                );
                None
            }
        }
    };

    // libmpv profile: https://github.com/mpv-player/mpv/blob/master/etc/builtin.conf#L21
    let mut args = vec![
        format!("--input-ipc-server={}", ipc_pipe),
        "--profile=libmpv".to_string(),
    ];

    if let Some(w) = wid {
        if parsed_wid.is_none() {
            args.push(format!("--wid={}", w));
        }
    }

    args.extend(mpv_config.args.iter().cloned());

    debug!("Using IPC pipe: {}", ipc_pipe);

    if let Some(w) = wid {
        debug!(
            "Starting mpv process for window '{}' (WID: {})",
            window_label, w
        );
    } else {
        debug!(
            "Starting mpv process for window '{}' (No WID)",
            window_label
        );
    }

    let mpv_path = mpv_config.path;

    debug!(
        "Spawning mpv process for window '{}' with args: {} {}",
        window_label,
        mpv_path,
        args.join(" ")
    );

    let args_clone = args.clone();
    let show_mpv_output = mpv_config.show_mpv_output;

    match Command::new(mpv_path.clone())
        .args(args)
        .stdout(Stdio::piped())
        .stderr(Stdio::null())
        .spawn()
    {
        Ok(mut child) => {
            let log_queue: Arc<Mutex<VecDeque<String>>> = Arc::new(Mutex::new(VecDeque::new()));
            let log_queue_clone = Arc::clone(&log_queue);
            let window_label_clone = window_label.to_string();

            if let Some(stdout) = child.stdout.take() {
                thread::spawn(move || {
                    let reader = BufReader::new(stdout);
                    for line in reader.lines().map_while(Result::ok) {
                        if show_mpv_output {
                            trace!("mpv stdout [{}] {}", window_label_clone, line);
                        }
                        if let Ok(mut queue) = log_queue_clone.lock() {
                            queue.push_back(line);
                            if queue.len() > 100 {
                                queue.pop_front();
                            }
                        }
                    }
                });
            }

            match wait_for_ipc_server(&ipc_pipe, ipc_timeout, window_label) {
                Ok(startup_duration) => {
                    info!(
                        "mpv IPC server for window '{}' is ready. Startup took {}ms.",
                        window_label,
                        startup_duration.as_millis()
                    );
                }
                Err(e) => {
                    let mut error_message = format!(
                        "mpv startup failed for window '{}'. Collected stdout:",
                        window_label,
                    );
                    error!("{}", error_message);
                    error_message.push('\n');
                    if let Ok(mut queue) = log_queue.lock() {
                        while let Some(line) = queue.pop_front() {
                            error!("mpv stdout [{}] {}", window_label, line);
                            error_message
                                .push_str(&format!("mpv stdout [{}] {}\n", window_label, line));
                        }
                    }
                    error!("{}", e);
                    error_message.push_str(&e);
                    let _ = child.kill();
                    return Err(crate::Error::MpvProcessError(error_message));
                }
            }

            info!(
                "mpv process (PID: {}) started for window '{}'. Initialization complete.",
                child.id(),
                window_label,
            );

            let window_label_clone = window_label.to_string();
            let observed_properties = mpv_config.observed_properties.clone();
            let app_clone = app.clone();
            let process_id = child.id();

            let instance = MpvInstance {
                process: child,
                ipc_timeout,
            };
            instances_lock.insert(window_label.to_string(), instance);

            drop(instances_lock);

            std::thread::spawn(move || {
                events::start_event_listener(
                    &app_clone,
                    process_id,
                    ipc_timeout,
                    observed_properties,
                    &window_label_clone,
                );
            });

            Ok(())
        }
        Err(e) => {
            let error_message = format!(
                "Failed to start mpv: {}. Is mpv installed and in your PATH?",
                e
            );
            error!("For window '{}': {}", window_label, error_message);
            debug!(
                "The command that failed for window '{}' was: {} {}",
                window_label,
                mpv_path,
                args_clone.join(" ")
            );
            Err(crate::Error::MpvProcessError(error_message))
        }
    }
}

pub fn kill_mpv_process<R: Runtime>(app: &AppHandle<R>, window_label: &str) -> crate::Result<()> {
    let instance_to_kill = {
        let mut instances_lock = app.mpv().instances.lock().unwrap();
        instances_lock.remove(window_label)
    };

    if let Some(mut instance) = instance_to_kill {
        info!(
            "Attempting to kill mpv process (PID: {}) for window '{}'...",
            instance.process.id(),
            window_label,
        );
        match instance.process.kill() {
            Ok(_) => {
                let _ = instance.process.wait();
                info!(
                    "mpv process (PID: {}) for window '{}' killed successfully.",
                    instance.process.id(),
                    window_label,
                );
                Ok(())
            }
            Err(e) => {
                let error_message = format!(
                    "Failed to kill mpv process (PID: {}) for window '{}': {}",
                    instance.process.id(),
                    window_label,
                    e,
                );
                error!("{}", error_message);
                Err(crate::Error::MpvProcessError(error_message))
            }
        }
    } else {
        info!(
            "No running mpv process found for window '{}' to kill. It may have already terminated.",
            window_label
        );
        Ok(())
    }
}

fn wait_for_ipc_server(
    ipc_pipe: &str,
    ipc_timeout: Duration,
    window_label: &str,
) -> Result<Duration, String> {
    let start = Instant::now();
    let pipe = Path::new(ipc_pipe);

    while start.elapsed() < ipc_timeout {
        if pipe.exists() {
            let elapsed = start.elapsed();
            return Ok(elapsed);
        }
        thread::sleep(Duration::from_millis(50));
    }

    Err(format!(
        "Timed out after {:?} waiting for IPC server for window '{}' at '{}'",
        ipc_timeout, window_label, ipc_pipe,
    ))
}