Skip to content

Instantly share code, notes, and snippets.

@hammerill
Last active June 15, 2025 17:15
Show Gist options
  • Save hammerill/08e43cbb8796acccc5f042df71427eef to your computer and use it in GitHub Desktop.
Save hammerill/08e43cbb8796acccc5f042df71427eef to your computer and use it in GitHub Desktop.
Chrome "Open in mpv"

Chrome "Open in mpv"

This is a detailed guide of adding a "Open in mpv" option to your right-click menu in Google Chrome (and potentially all Chromium-based browsers).

Prerequisites

  • The mpv player itself.
  • python3.
  • yt-dlp.

If you're on macOS, install these with Brew.

Where Does It Work

This method was tested:

  • on Arch Linux with Google Chrome installed via AUR.
  • on macOS.

Test for Windows is coming soon.

Your feedback is needed to fix all the eventual bugs!

How to Install

  1. First, create a Python script "open_in_mpv.py" and save it in your preferred location, e.g. ~/Scripts/open_in_mpv.py:
#!/usr/bin/env python3

import sys
import json
import struct
import subprocess
import platform

CREATE_NO_WINDOW = 0x08000000

def get_message():
    """Read a message from stdin and decode it."""
    raw_length = sys.stdin.buffer.read(4)

    if not raw_length:
        sys.exit(0)

    message_length = struct.unpack("=I", raw_length)[0]
    message = sys.stdin.buffer.read(message_length).decode("utf-8")

    return json.loads(message)

try:
    url = get_message()

    if platform.system() == "Linux":
        command = ["mpv", url]

        subprocess.Popen(command, 
                         stdout=subprocess.DEVNULL, 
                         stderr=subprocess.DEVNULL)

    elif platform.system() == "Darwin":
        BIN_PATH = "/opt/homebrew/bin"
        command = [f"{BIN_PATH}/mpv", f"--script-opts=ytdl_hook-ytdl_path={BIN_PATH}/yt-dlp", url]

        subprocess.Popen(command, 
                         stdout=subprocess.DEVNULL, 
                         stderr=subprocess.DEVNULL)

    elif platform.system() == "Windows":
        command = ["mpv", url]

        subprocess.Popen(command, 
                         stdout=subprocess.DEVNULL, 
                         stderr=subprocess.DEVNULL,
                         creationflags=CREATE_NO_WINDOW)
except Exception as e:
    with open("/tmp/mpv_chrome_host.log", "a") as f:
        import traceback
        import datetime
        f.write(f"Timestamp: {datetime.datetime.now()}\n")
        f.write(f"Error: {e}\n")
        f.write(traceback.format_exc())
        f.write("------\n")
  1. Make it executable (only Linux/macOS):
chmod +x ~/Scripts/open_in_mpv.py # Adjust your script location
  1. Create an unpacked extension in some temporary directory. First, manifest.json:
{
  "name": "Open in mpv",
  "version": "1.0",
  "manifest_version": 3,
  "description": "Right-click to open links in mpv player.",
  "permissions": [
    "contextMenus",
    "nativeMessaging"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "icons": {
    "128": "mpv-icon.png"
  }
}
  1. Then, in the same directory, download mpv icon and rename it to mpv-icon.png.

  2. Finally, in this same directory, background.js:

const NATIVE_HOST_NAME = "com.hammerill.mpv_host";

chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: "open-in-mpv",
    title: "Open in mpv",
    contexts: ["link", "video", "audio", "page"],
  });
});

chrome.contextMenus.onClicked.addListener((info) => {
  if (info.menuItemId === "open-in-mpv") {
    const url = info.linkUrl || info.pageUrl;

    if (url) {
      const port = chrome.runtime.connectNative(NATIVE_HOST_NAME);

      port.onDisconnect.addListener(() => {
        if (chrome.runtime.lastError) {}
      });

      port.postMessage(url);
    }
  }
});
  1. Open chrome://extensions.
  2. Enable "Developer mode".
  3. Click on "Load unpacked".
  4. Select the directory where you just created "manifest.json", "mpv-icon.png" and "background.js".
  5. Note the ID of the new "Open in mpv" extension, a long string of letters.
  6. Add a new Chrome native messaging host (a JSON). Its location will be different across systems:
  • Linux: ~/.config/google-chrome/NativeMessagingHosts/com.hammerill.mpv_host.json.
  • macOS: ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.hammerill.mpv_host.json.
  • Windows: anywhere you want, but in regedit, set the HKEY_CURRENT_USER\SOFTWARE\Google\Chrome\NativeMessagingHosts\com.hammerill.mpv_host key "(Default)" value to its path.

And fill it with this:

{
  "name": "com.hammerill.mpv_host",
  "description": "Host for sending URLs to MPV",
  "path": "/home/YOUR_USERNAME/Scripts/open_in_mpv.py",
  "type": "stdio",
  "allowed_origins": [
    "chrome-extension://YOUR_EXTENSION_ID/"
  ]
}

Replacing YOUR_USERNAME and YOUR_EXTENSION_ID with actual values.

  1. Restart Chrome and see if it works. You probably can open it in terminal to see eventual errors.

Credits

This script was built with help of Google Gemini 2.5 Pro. I leave the whole conversation of me and this LLM here for serenity:

See conv.md just below.

The conversation between me and Gemini

User

I want to add a "Open in mpv" option to my right click menu in google chrome


Assistant

Of course! Adding an "Open in mpv" option to Chrome's right-click menu is a fantastic way to streamline your workflow.

Because of browser security restrictions, a web page (and its extensions) cannot directly execute a program on your computer. To bridge this gap, you need a two-part solution:

  1. A Chrome Extension: This adds the option to your right-click menu.
  2. A Native Messaging Host: A small helper program on your computer that the extension communicates with. The extension sends the URL to this host, and the host is what actually launches mpv.

Here are two methods to get this set up, from easiest to most advanced.


Method 1: The Easy Way (Recommended)

The simplest approach is to use a pre-built extension and host that someone has already created. The most popular and well-maintained one is "Play with mpv".

Step 1: Install the Chrome Extension

Go to the Chrome Web Store and install the "Play with mpv" extension.

Step 2: Install the Native Host Helper

The extension itself can't launch mpv. You must download and install its companion helper script.

  1. Go to the official GitHub repository for the helper script:

  2. Click on the green < > Code button and select Download ZIP.

  3. Extract the downloaded ZIP file to a permanent location on your computer (e.g., inside your Documents or a "Scripts" folder).

  4. Run the installation script for your operating system:

    • On Windows:

      • Navigate into the extracted folder.
      • Right-click on install-host.bat and select "Run as administrator".
      • A command prompt will appear and register the native host.
    • On macOS / Linux:

      • Open your Terminal.
      • Navigate to the extracted folder using the cd command. For example: cd ~/Downloads/play-with-mpv-handler-main
      • Make the script executable: chmod +x install-host.sh
      • Run the installer: ./install-host.sh
      • You might be prompted for your password if it needs to install to a system-wide directory.

Step 3: Ensure mpv is in your System's PATH

For the helper to find mpv, the mpv executable must be in your system's PATH.

  • Windows: When installing mpv (e.g., via Scoop, Chocolatey, or from a pre-compiled build), there is often an option to add it to the PATH. If not, you'll need to add the folder containing mpv.exe to your Environment Variables manually.
  • macOS: If you installed mpv using Homebrew (brew install mpv), it's already in your PATH.
  • Linux: If you installed mpv using your distribution's package manager (sudo apt install mpv, sudo dnf install mpv, etc.), it's already in your PATH.

Step 4: Use It!

You're all set! Now you can:

  • Right-click on any link that points to a video or audio file.
  • Right-click on a YouTube, Twitch, or other supported video page.
  • Select "Play with mpv".

mpv should launch and start playing the content.


Method 2: The DIY / Advanced Way

If you prefer not to use a third-party extension or want to understand how it works, you can build a minimal version yourself. This is more involved.

Part A: Create the Native Host Script

This script will receive the URL from Chrome and launch mpv. We'll use Python as it's simple and cross-platform.

  1. Create a file named open_in_mpv.py and paste the following code into it:

    #!/usr/bin/env python3
    
    import sys
    import json
    import struct
    import subprocess
    
    # On Windows, we need to set the process creation flags to hide the console window
    # that would otherwise pop up when we run mpv.
    # See: https://docs.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
    CREATE_NO_WINDOW = 0x08000000
    
    def get_message():
        """Read a message from stdin and decode it."""
        raw_length = sys.stdin.buffer.read(4)
        if not raw_length:
            sys.exit(0)
        message_length = struct.unpack('=I', raw_length)[0]
        message = sys.stdin.buffer.read(message_length).decode('utf-8')
        return json.loads(message)
    
    try:
        # Get the URL sent by the extension
        url = get_message()
    
        # The command to execute. Add any custom mpv flags here.
        # For example: ['mpv', '--ytdl-format=best', url]
        command = ['mpv', url]
    
        # Launch mpv in a new process, detached from our script.
        # Use CREATE_NO_WINDOW flag on Windows to prevent a cmd.exe flash.
        if sys.platform == "win32":
            subprocess.Popen(command, creationflags=CREATE_NO_WINDOW)
        else:
            subprocess.Popen(command)
    
    except Exception as e:
        # For debugging: if something goes wrong, write it to a file.
        # You can comment this out in production.
        with open('/tmp/mpv_chrome_host.log', 'a') as f:
            f.write(f"Error: {e}\n")
  2. Save this script in a permanent location (e.g., C:\Users\YourUser\Scripts\ or ~/bin/).

Part B: Create the Host Manifest JSON

Chrome needs a manifest file to know where your script is and which extension is allowed to talk to it.

  1. Create a file named com.my.mpv_host.json and paste the following into it:

    {
      "name": "com.my.mpv_host",
      "description": "Host for sending URLs to MPV",
      "path": "C:\\Path\\To\\Your\\open_in_mpv.py",
      "type": "stdio",
      "allowed_origins": [
        "chrome-extension://PASTE_YOUR_EXTENSION_ID_HERE/"
      ]
    }
  2. Crucially, you must edit two things:

    • path: Replace "C:\\Path\\To\\Your\\open_in_mpv.py" with the full, absolute path to your Python script.
      • Windows: Use double backslashes (\\). Example: "C:\\Users\\Me\\Scripts\\open_in_mpv.py"
      • macOS/Linux: Use forward slashes (/). Example: "/home/me/bin/open_in_mpv.py"
    • allowed_origins: We will fill this in after creating the extension. Leave it as a placeholder for now.

Part C: Create the Chrome Extension

  1. Create a new folder for your extension (e.g., my-mpv-extension).

  2. Inside that folder, create a file named manifest.json:

    {
      "name": "DIY Open in mpv",
      "version": "1.0",
      "manifest_version": 3,
      "description": "Right-click to open links in mpv player.",
      "permissions": [
        "contextMenus",
        "nativeMessaging"
      ],
      "background": {
        "service_worker": "background.js"
      }
    }
  3. In the same folder, create a file named background.js:

    // The name of our native host, from its manifest file.
    const NATIVE_HOST_NAME = "com.my.mpv_host";
    
    // Create the context menu item.
    chrome.runtime.onInstalled.addListener(() => {
      chrome.contextMenus.create({
        id: "open-in-mpv",
        title: "Open Link in mpv",
        contexts: ["link", "video", "audio", "page"] // Show on links, media, and pages
      });
    });
    
    // Handle the click event.
    chrome.contextMenus.onClicked.addListener((info, tab) => {
      if (info.menuItemId === "open-in-mpv") {
        // The URL is in info.linkUrl if a link was clicked,
        // or info.pageUrl if the page background was clicked.
        const url = info.linkUrl || info.pageUrl;
    
        if (url) {
          // Connect to the native host and send the URL.
          const port = chrome.runtime.connectNative(NATIVE_HOST_NAME);
          port.postMessage(url);
        }
      }
    });

Part D: Put It All Together

  1. Install the Extension:

    • Open Chrome and go to chrome://extensions.
    • Enable "Developer mode" in the top-right corner.
    • Click "Load unpacked" and select the folder you created for your extension (my-mpv-extension).
    • Your extension will appear. Copy its ID (a long string of random letters).
  2. Update the Host Manifest:

    • Go back to your com.my.mpv_host.json file.
    • Replace "PASTE_YOUR_EXTENSION_ID_HERE" with the ID you just copied. The line should now look like this: "chrome-extension://abcdefghijklmnopqrstuvwxyz123456/".
  3. Register the Native Host:

    • This is the final step, and it's platform-specific. You need to tell Chrome where your host manifest is.

    • Windows: Create a registry key. Open regedit and navigate to HKEY_CURRENT_USER\SOFTWARE\Google\Chrome\NativeMessagingHosts\.

      • Create a new key named com.my.mpv_host.
      • Set its (Default) value to the full path of your com.my.mpv_host.json file (e.g., C:\Users\Me\Scripts\com.my.mpv_host.json).
    • macOS:

      • Place the com.my.mpv_host.json file in: ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/
    • Linux:

      • Place the com.my.mpv_host.json file in: ~/.config/google-chrome/NativeMessagingHosts/ (or ~/.config/chromium/... for Chromium).
  4. Restart Chrome and test it out! You should now have your own "Open Link in mpv" option in the right-click menu.


User

I proceeded with diy method on linux. The option appears but doesn't do anything.

This is what i see in the logs:

[256033:256062:0608/164626.811912:ERROR:chrome/browser/extensions/api/messaging/native_message_process_host.cc:308] Native Messaging host tried sending a message that is 1701407843 bytes long.

Assistant

Excellent, you've diagnosed the problem perfectly! That log message is the key.

Let's break down what it means and how to fix it.

The Root Cause

The error message Native Messaging host tried sending a message that is 1701407843 bytes long tells us exactly what's wrong.

  1. The Protocol: The native messaging protocol expects communication from the host script (your Python script) back to Chrome to be in a specific format: a 4-byte number indicating the message length, followed by the message itself (as JSON).
  2. The Problem: Your Python script launches mpv. By default, the mpv process inherits the standard output (stdout) of its parent (the Python script). mpv is very "chatty" and prints status information to stdout as it plays a video (e.g., (+) Video --vid=1..., progress updates, etc.).
  3. The Misinterpretation: Chrome receives this text from mpv. It tries to interpret it as a valid native message. It reads the first 4 bytes of mpv's output, gets a garbage value (which translates to the absurdly large 1701407843), and immediately terminates the connection because it knows a 1.7 GB message is not valid.

In short: mpv's normal output is being accidentally sent back to Chrome, breaking the communication.


The Fix: Redirect mpv's Output

The solution is to tell the subprocess command to launch mpv but throw away its output instead of letting it bubble up to Chrome. We do this by redirecting its stdout and stderr to DEVNULL.

Here is the corrected open_in_mpv.py script.

Replace the entire content of your open_in_mpv.py with this:

#!/usr/bin/env python3

import sys
import json
import struct
import subprocess

# On Windows, we need to set the process creation flags to hide the console window
# that would otherwise pop up when we run mpv.
CREATE_NO_WINDOW = 0x08000000

def get_message():
    """Read a message from stdin and decode it."""
    raw_length = sys.stdin.buffer.read(4)
    if not raw_length:
        sys.exit(0)
    message_length = struct.unpack('=I', raw_length)[0]
    message = sys.stdin.buffer.read(message_length).decode('utf-8')
    return json.loads(message)

try:
    url = get_message()
    command = ['mpv', url]

    # --- THIS IS THE CRUCIAL FIX ---
    # Launch mpv, but redirect its stdout and stderr to DEVNULL (a black hole).
    # This prevents mpv's status messages from being sent back to Chrome.
    if sys.platform == "win32":
        subprocess.Popen(command, 
                         stdout=subprocess.DEVNULL, 
                         stderr=subprocess.DEVNULL,
                         creationflags=CREATE_NO_WINDOW)
    else:
        subprocess.Popen(command, 
                         stdout=subprocess.DEVNULL, 
                         stderr=subprocess.DEVNULL)

except Exception as e:
    # This logging is very useful for debugging!
    # Check this file if things still don't work.
    with open('/tmp/mpv_chrome_host.log', 'a') as f:
        # Get the full traceback for better error info
        import traceback
        f.write(f"Timestamp: {datetime.datetime.now()}\n")
        f.write(f"Error: {e}\n")
        f.write(traceback.format_exc())
        f.write("------\n")

Next Steps to Ensure it Works

After updating the Python script, follow these steps:

1. Make the Script Executable

This is a very common gotcha on Linux. The host manifest will try to execute the script, but it will fail if it doesn't have the execute permission.

Open a terminal, navigate to where you saved open_in_mpv.py, and run:

chmod +x open_in_mpv.py

2. Check the Shebang Line

The first line of the script (#!/usr/bin/env python3) tells the system to use the python3 interpreter. Make sure python3 is installed and in your system's PATH. You can check this by running which python3 in your terminal. If it returns a path, you're good.

3. Restart Chrome

You don't always need to, but it's the surest way to make Chrome re-read everything and re-establish the connection to the native host. Completely quit Chrome and reopen it.

4. Test It

Right-click on a video link and select your "Open in mpv" option. It should now launch mpv without any issues.

5. If It Still Fails...

Check your debug log file. The try...except block in the Python script is designed to catch any other errors and write them to /tmp/mpv_chrome_host.log. For example, it would log an error if it couldn't find the mpv command.

cat /tmp/mpv_chrome_host.log

This will show you if the Python script itself is encountering an error.


User

It seems to work now.

I tried this on macOS. It works when Chrome is launched from within terminal. When launched as usual, it errors with:

Unchecked runtime.lastError: Native host has exited.

With the context:

:0 (anonymous function)

Assistant

Excellent diagnosis! This is a classic and very common issue on macOS, and you've pinpointed the exact symptom that reveals the cause.

The Problem: The PATH Environment Variable

When you launch an application on macOS, its environment is inherited differently depending on how you launch it:

  1. Launched from Terminal: When you open a terminal, it runs your shell's startup script (e.g., ~/.zshrc or ~/.bash_profile). These scripts typically configure your PATH environment variable to include locations like /opt/homebrew/bin (for Apple Silicon) or /usr/local/bin (for Intel), which is where Homebrew installs programs like mpv. When you launch Chrome from this terminal, Chrome inherits this fully-configured environment, including the correct PATH.

  2. Launched from GUI (Dock/Finder): When you click the Chrome icon in the Dock or Applications folder, it's launched by the system's launchd process. It inherits a very minimal, default environment. This default PATH does not include the Homebrew directories.

The result:

  • In the terminal case, your Python script runs, calls subprocess.Popen(['mpv', ...]), the system checks the PATH, finds mpv in the Homebrew directory, and everything works.
  • In the GUI case, the Python script runs, calls subprocess.Popen(['mpv', ...]), the system checks the very limited PATH, cannot find mpv, and the Python script immediately fails with a FileNotFoundError.

From Chrome's perspective, it just sees that the native host process started and then immediately died, which is exactly what the Native host has exited error means.


Solution 1 (Recommended): Use the Absolute Path in the Script

This is the most direct, reliable, and self-contained fix. Instead of relying on the PATH variable, we'll tell the script exactly where to find mpv.

1. Find the full path to mpv: Open your terminal and run:

which mpv

This will output the full path. It will likely be one of these:

  • /opt/homebrew/bin/mpv (if you are on an Apple Silicon Mac)
  • /usr/local/bin/mpv (if you are on an Intel Mac)

2. Update your open_in_mpv.py script: Replace the line that defines the command with the full path you just found.

BEFORE:

command = ['mpv', url]

AFTER (for an Apple Silicon Mac):

command = ['/opt/homebrew/bin/mpv', url]

OR AFTER (for an Intel Mac):

command = ['/usr/local/bin/mpv', url]

Your modified try block should now look like this (using the Apple Silicon path as an example):

try:
    url = get_message()
    # Use the full, absolute path to the mpv executable
    command = ['/opt/homebrew/bin/mpv', url] 

    if sys.platform == "win32":
        # ... (rest of the script is unchanged)
    else:
        subprocess.Popen(command, 
                         stdout=subprocess.DEVNULL, 
                         stderr=subprocess.DEVNULL)
except Exception as e:
    # ... (logging code is unchanged)

3. Save the script and restart Chrome. This change will now work regardless of how Chrome is launched, because you are no longer depending on the fragile PATH environment variable.


Solution 2 (Advanced): Set the Environment for GUI Apps

If you want to fix this "system-wide" for all GUI applications, you can tell macOS to add the Homebrew path to the environment for all apps launched from the GUI.

This is more complex and generally not necessary if you only need it for this one script.

The modern way to do this is with launchctl.

  1. Open Terminal.

  2. Run the following command, using the correct path for your Mac's architecture (/opt/homebrew/bin for Apple Silicon, /usr/local/bin for Intel). This command sets the PATH for all subsequent GUI application launches in your current login session.

    # For Apple Silicon
    launchctl setenv PATH "/opt/homebrew/bin:$(launchctl getenv PATH)"
    
    # For Intel
    launchctl setenv PATH "/usr/local/bin:$(launchctl getenv PATH)"
  3. To make this permanent across reboots, you need to create a Launch Agent.

    • Create a file at ~/Library/LaunchAgents/environment.plist.
    • Paste the following content into it, again, editing the path to match your system.
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
      <key>Label</key>
      <string>my.startup.environment</string>
      <key>ProgramArguments</key>
      <array>
        <string>sh</string>
        <string>-c</string>
        <!-- Use the correct path for your Mac -->
        <string>launchctl setenv PATH "/opt/homebrew/bin:$(launchctl getenv PATH)"</string>
      </array>
      <key>RunAtLoad</key>
      <true/>
    </dict>
    </plist>
  4. Log out and log back in, or reboot your Mac. Your GUI apps will now have the Homebrew path.

For its simplicity and reliability, I strongly recommend Solution 1.


User

I proceeded with solution 1. The script stopped reporting that 'mpv' is unaccessible.

But as seen from Chrome, the problem stays the same. mpv doesn't appear and the native host runtime error is still there.

But I see something bizarre: upon clicking on Open in mpv option, there's a macOS "playing now" button that appears, contains nothing (a blank Apple Music ui) and then disappears.


Assistant

That's a fantastic and crucial clue! The "Now Playing" widget appearing and disappearing is the smoking gun.

Here's what's happening now:

  1. Success: Your previous fix worked. The Python script is no longer failing because it can't find mpv. It is now successfully executing /opt/homebrew/bin/mpv.
  2. The New Problem: The mpv process itself is starting, but then immediately quitting.
  3. The Clue: The "Now Playing" widget is controlled by a macOS system service. When mpv starts, it registers itself as a media player, which causes the widget to appear. When mpv quits a fraction of a second later, the widget disappears.
  4. The Cause: When launched from a non-interactive context like your Python script (which is launched by Chrome), mpv sometimes fails to properly attach to the macOS WindowServer to create its video window. It doesn't find a display context it likes, so it concludes it cannot play the video and exits cleanly. Because it exits without an error, your Python script also finishes its job and exits cleanly. From Chrome's perspective, the host process simply vanished, leading to the same Native host has exited error.

The Solution: Force mpv to Create a Window

We need to give mpv a stronger hint that yes, it is absolutely supposed to run as a GUI application and create a window. We can do this with a command-line flag. We'll also add another flag for robustness.

1. Edit your open_in_mpv.py script again.

2. Modify the line that defines the command variable.

Add the --force-window and --idle=yes flags before the URL.

BEFORE:

command = ['/opt/homebrew/bin/mpv', url]

AFTER (for an Apple Silicon Mac):

command = ['/opt/homebrew/bin/mpv', '--force-window', '--idle=yes', url]

OR AFTER (for an Intel Mac):

command = ['/usr/local/bin/mpv', '--force-window', '--idle=yes', url]

What do these flags do?

  • --force-window: This is the key. It explicitly tells mpv to create a playback window even if it thinks it can't or shouldn't. This usually solves the issue of it quitting when run from a non-terminal context.
  • --idle=yes: This is a great flag for robustness. It tells mpv to start up and stay running (in an idle state) even if there's nothing to play. This can prevent it from quitting immediately if there's a slight delay in resolving the URL.

Your full try block should now look something like this:

try:
    url = get_message()
    # Add flags to ensure a window is created and mpv stays open
    command = ['/opt/homebrew/bin/mpv', '--force-window', '--idle=yes', url]

    if sys.platform == "win32":
        subprocess.Popen(command,
                         stdout=subprocess.DEVNULL,
                         stderr=subprocess.DEVNULL,
                         creationflags=CREATE_NO_WINDOW)
    else:
        subprocess.Popen(command,
                         stdout=subprocess.DEVNULL,
                         stderr=subprocess.DEVNULL)
except Exception as e:
    # ... (logging code remains the same)

Next Steps

  1. Save the modified open_in_mpv.py file.
  2. Completely quit and restart Chrome. This is important to make sure any old, cached versions of the host script are gone.
  3. Test it again by right-clicking a link.

This time, mpv should be forced to create its window, and it should launch and play the video as expected, even when Chrome is started from the Dock or Finder.


User

mpv appears now. But it's empty. The window title says "No file - mpv".


Assistant

This is progress! We are getting very close.

This symptom—mpv launching but being empty—tells us something extremely specific:

  • The Chrome extension, the native host registration, and the Python script are all working correctly to launch the mpv process.
  • The problem is that the URL is not being passed from the Python script to the mpv process. The url variable in your script is likely empty or None.

Let's find out why. We need to trace the data from Chrome to Python.

Step 1: Add Debug Logging to Your Python Script

This is the most important step. We need to see exactly what message Chrome is sending to our script.

Modify your open_in_mpv.py to log the received URL before trying to use it.

#!/usr/bin/env python3

import sys
import json
import struct
import subprocess
import datetime # Import datetime for logging

# ... (get_message function remains the same) ...

def get_message():
    """Read a message from stdin and decode it."""
    raw_length = sys.stdin.buffer.read(4)
    if not raw_length:
        sys.exit(0)
    message_length = struct.unpack('=I', raw_length)[0]
    message = sys.stdin.buffer.read(message_length).decode('utf-8')
    return json.loads(message)

# Define the log file path
LOG_FILE = '/tmp/mpv_chrome_host.log'

try:
    # Get the URL sent by the extension
    url = get_message()

    # --- THIS IS THE NEW DEBUGGING CODE ---
    # Log exactly what we received from Chrome
    with open(LOG_FILE, 'a') as f:
        f.write(f"--- {datetime.datetime.now()} ---\n")
        f.write(f"Received URL: '{url}' (Type: {type(url)})\n")
    # --- END OF DEBUGGING CODE ---

    # Only proceed if the URL is not empty
    if url:
        command = [
            '/opt/homebrew/bin/mpv', # Or /usr/local/bin/mpv for Intel
            '--force-window',
            '--idle=yes',
            url
        ]

        subprocess.Popen(command,
                         stdout=subprocess.DEVNULL,
                         stderr=subprocess.DEVNULL)

except Exception as e:
    # This will catch errors in get_message() or subprocess
    with open(LOG_FILE, 'a') as f:
        import traceback
        f.write(f"--- {datetime.datetime.now()} ---\n")
        f.write(f"An error occurred:\n")
        f.write(traceback.format_exc())

Step 2: Test and Check the Log

  1. Save the updated Python script.
  2. Completely quit and restart Chrome.
  3. Right-click on a video link or a YouTube page and select "Open in mpv".
  4. mpv will likely open empty again. That's okay.
  5. Now, open your Terminal and check the log file:
    cat /tmp/mpv_chrome_host.log

You will most likely see something like: Received URL: '' (Type: <class 'str'>)

This would confirm that Chrome is sending an empty string to your script.

Step 3: Fix the Chrome Extension (The Likely Cause)

The issue is almost certainly in how the background.js script is capturing the URL. When you right-click on different things, Chrome provides the URL in different properties of the info object.

  • info.linkUrl: For <a href="..."> links.
  • info.srcUrl: For media elements like <video>, <img>, or <audio>. This is crucial for embedded players.
  • info.pageUrl: The URL of the current page. This is the fallback for sites like YouTube.

Your current background.js probably only checks for one or two of these. Let's make it robust by checking for all of them in the correct order of priority.

Replace the entire content of your background.js with this:

// The name of our native host, from its manifest file.
const NATIVE_HOST_NAME = "com.my.mpv_host";

// Create the context menu item when the extension is installed.
chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: "open-in-mpv",
    title: "Open in mpv",
    // Make the option available on links, pages, and media elements
    contexts: ["link", "page", "video", "audio"]
  });
});

// Handle the click event.
chrome.contextMenus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === "open-in-mpv") {
    // Determine the URL in order of preference:
    // 1. A direct link URL.
    // 2. The source URL of a media element (e.g., a <video> tag).
    // 3. The URL of the page itself (for sites like YouTube).
    const url = info.linkUrl || info.srcUrl || info.pageUrl;

    if (url) {
      console.log("Sending URL to native host:", url); // For debugging in the extension's console
      chrome.runtime.connectNative(NATIVE_HOST_NAME).postMessage(url);
    } else {
      console.error("Could not determine a URL to send to mpv.");
    }
  }
});

Final Steps to Apply the Fix

  1. Save the new background.js file.
  2. Go back to the Chrome Extensions page: chrome://extensions.
  3. Find your "DIY Open in mpv" extension.
  4. Click the reload icon (a circular arrow) on the extension's card. This forces Chrome to load the new background.js file.
  5. Try right-clicking again. It should now work for a much wider variety of content.

This combination of better logging in the Python script and more robust URL detection in the JavaScript should solve the problem for good.


User

I added the url logging. It's there. The script knows the URL to play.


Assistant

Okay, this is a fantastic piece of information. Your debugging has successfully isolated the final link in the chain.

Let's recap what we know:

  1. Chrome -> Python: Works. The background.js sends the URL.
  2. Python Script: Works. The script receives the URL and logs it correctly.
  3. Python -> mpv Launch: Works. The subprocess.Popen call successfully starts the mpv process with its window.

The fact that mpv starts but shows "No file" while your script knows the URL means mpv itself is failing to process the URL you're giving it.

The Diagnosis: The yt-dlp Helper is Missing

When you give mpv a URL from a site like YouTube, Twitch, etc., it doesn't download the video itself. It uses a helper program to extract the direct video stream URL. This helper is almost always yt-dlp (or its predecessor, youtube-dl).

This is a classic "chain of failure" problem, identical to the one we solved for mpv itself:

  1. You launch Chrome from the GUI, so it has a minimal PATH.
  2. Your Python script inherits this minimal PATH.
  3. mpv is launched by Python and it also inherits the same minimal PATH.
  4. When mpv gets the YouTube URL, it tries to execute yt-dlp to resolve it.
  5. It looks for yt-dlp in its minimal PATH, can't find it, and gives up on the URL.
  6. It remains open (because of --idle=yes) but has no file to play.

Quick Confirmation Test (Optional)

If you right-click on a link to a direct video file (e.g., a URL ending in .mp4 or .mkv), it will probably work perfectly. This is because mpv can handle direct file URLs without needing yt-dlp. If this test passes, the diagnosis is 100% confirmed.

The Solution: Tell mpv Where to Find yt-dlp

Just like we gave mpv its own full path, we need to give it the full path to its helper.

1. Find the full path to yt-dlp: First, make sure it's installed (brew install yt-dlp if not). Then, find its path by running this in your terminal:

which yt-dlp

It will output something like /opt/homebrew/bin/yt-dlp (Apple Silicon) or /usr/local/bin/yt-dlp (Intel).

2. Update your open_in_mpv.py script: We will add a special option to mpv to tell it exactly where to find the yt-dlp executable.

The mpv option is: --script-opts=ytdl_hook-ytdl_path=/path/to/your/yt-dlp

Modify the command list in your script to include this new option.

BEFORE:

command = [
    '/opt/homebrew/bin/mpv',
    '--force-window',
    '--idle=yes',
    url
]

AFTER (using Apple Silicon path as an example):

command = [
    '/opt/homebrew/bin/mpv',
    '--force-window',
    '--idle=yes',
    # Explicitly tell mpv where to find its helper utility
    '--script-opts=ytdl_hook-ytdl_path=/opt/homebrew/bin/yt-dlp',
    url
]

Your final, fully robust try block should now look like this:

try:
    url = get_message()

    # Log what we received
    with open(LOG_FILE, 'a') as f:
        f.write(f"--- {datetime.datetime.now()} ---\n")
        f.write(f"Received URL: '{url}'\n")

    if url:
        # Use absolute paths for both mpv and its yt-dlp helper
        # to ensure it works when launched from the GUI.
        command = [
            '/opt/homebrew/bin/mpv',  # Or /usr/local/bin/mpv for Intel
            '--force-window',
            '--idle=yes',
            '--script-opts=ytdl_hook-ytdl_path=/opt/homebrew/bin/yt-dlp', # Or /usr/local/bin/yt-dlp
            url
        ]

        subprocess.Popen(command,
                         stdout=subprocess.DEVNULL,
                         stderr=subprocess.DEVNULL)

except Exception as e:
    # ... (logging code) ...

Final Steps

  1. Save the updated open_in_mpv.py.
  2. Completely quit and restart Chrome.
  3. Test it on a YouTube page.

This should be the final piece of the puzzle. By removing all reliance on the inherited PATH variable for every program in the chain, you create a fully self-contained and reliable script that will work no matter how Chrome is launched.

@for-coursera
Copy link

Would be nice also to be able to pass the current page to MPV (just in case one hasn't been able to do that via a right click on the link beforehand). Seems like adding some additional context ("page"?) will suffice?

@hammerill
Copy link
Author

hammerill commented Jun 10, 2025

Would be nice also to be able to pass the current page to MPV (just in case one hasn't been able to do that via a right click on the link beforehand). Seems like adding some additional context ("page"?) will suffice?

@for-coursera Oh you're right, it was actually there but I decided to delete it because I couldn't see how it could be useful. I can bring it back!

@for-coursera
Copy link

for-coursera commented Jun 10, 2025

I can bring it back!

@hammerill It's up to you, and certainly not a problem if you personally don't see it useful, this our discussion will anyway help those, who'd like to have it :)

@hammerill
Copy link
Author

hammerill commented Jun 15, 2025

UPD 2025-06-15: Chrome just got updated on macOS and mpv is not opening anymore. Investigating now...

UPD2: It seemed like a little complain from YouTube which required cookies from yt-dlp. It's ok now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment