External Player Launcher

:placard: Summary This plugin allows you to launch external media players directly from scene cards and scene detail pages to play videos.
:link: Repository https://github.com/esumaka/stash-plugin-repo/tree/main/plugins/external-player-launcher
:information_source: Source URL https://esumaka.github.io/stash-plugin-repo/main/index.yml
:open_book: Install How to install a plugin?

:package: Source Code: https://github.com/esumaka/external-player-launcher

External Player Launcher

This plugin allows you to launch external media players directly from scene cards and scene detail pages to play videos.

Because browsers only support a limited set of video formats, some formats (such as AVI, MKV, and RMVB) often require transcoding before playback.

This plugin hands playback off to external media players to bypass browser format limitations, allowing you to play a wider range of video formats without transcoding. You can also use the full feature set of your media player—including subtitles, audio tracks, playback controls, and more—while avoiding transcoding delays and quality loss.

Features

  • Adds a player dropdown at the bottom of scene cards for quickly choosing an external player.
  • When Single Player Mode is enabled, the player dropdown is replaced by a single player button for one-click playback.
  • Supports launching an external player from scene detail pages.
  • Lets you show or hide individual player buttons in settings as needed.
  • Lets you independently control whether player buttons are shown on scene cards, in the Players tab, and in the toolbar on scene detail pages.
  • Settings are stored only in the current browser, so you can configure players independently on different devices.

Screenshots

Scene card:

Scene card (single player mode):

Scene detail page:

Scene detail page toolbar (single player mode):

Supported Players

The currently supported players and their platforms:

:warning: Note: Some players require additional tools to function properly.

Player Windows Android iOS macOS Linux
IINA :white_check_mark:
Infuse :white_check_mark: :white_check_mark:
MPC-HC :white_check_mark: (requires mpc-protocol)
MPV :white_check_mark: (requires mpv-handler) :white_check_mark: :white_check_mark: :white_check_mark: (requires mpv-handler)
MX Player (Pro) :white_check_mark:
nPlayer :white_check_mark: :white_check_mark:
PotPlayer :white_check_mark:
VLC :white_check_mark: (requires vlc-protocol) :white_check_mark: :white_check_mark: :white_check_mark: (requires vlc-protocol) :white_check_mark: (requires vlc-protocol)

Installing the Plugin

  1. In Stash, open Settings → Plugins.
  2. Click Add Source and fill in the following:
    • Name: esumaka plugin repo
    • Source URL: https://esumaka.github.io/stash-plugin-repo/main/index.yml
  3. Click Confirm to add the source.
  4. Select External Player Launcher from the list of Available Plugins and click Install.
  5. Refresh the Stash page for the plugin to take effect.

Usage

  • Make sure a supported player is installed. See Supported Players.
  • Click the player icon at the bottom of a scene card, then select the desired player from the menu.
  • Alternatively, open a scene detail page, switch to the Players tab, and click the desired player button.
  • Your browser may display a dialog like “trying to open xxx?”. Click Open to proceed.

Warning

This plugin may conflict with other plugins that modify scene cards, causing player buttons to disappear or appear in the wrong position.

3 Likes

A couple feature requests that should hopefully be pretty easy to implement:

  • Instance-wide default settings. While it’s good that the specific players are client-specific, it’s generally the case that most of a particular user’s clients will share a baseline common set of applications. Being able to set global defaults would reduce the per-client configuration needed.
  • Per-client hiding. Allow a state where no players are enabled and the scene-card button is hidden. I want to be able to use VLC on my Windows machine without cluttering my Chromebook.

Thanks for the suggestions!

Regarding global settings, I won’t be implementing this feature in the short term. To be honest, it’s a bit complex and would likely require refactoring the code with significant changes. Maybe someday in the future I’ll implement it. But even if you have many clients, you usually only need to configure each one once.

As for hiding the player button on scene cards, I should be adding that in the next release soon.

Updated to Version 1.1.0

  • Added “Entry Points” settings group to independently control the visibility of player buttons on scene cards and scene detail pages
  • Reorganized settings UI into groups (Entry Points / Player Settings) for a cleaner layout

@keyboardr Now you can hide the scene-card player button by disable the “Show scene card player buttons” option in settings.

Thanks for the plugin!

A couple of suggestions:

  • Can the scene page also have “launch button”, instead of a “tab” with the player configuration, etc? This would save a click, and settings for the plugin are also available in the stash’s settings page. If needed, this could be optional. E.g. something like this
diff -urpN external-player-launcher.bak/assets/locales/en-US.json external-player-launcher/assets/locales/en-US.json
--- external-player-launcher.bak/assets/locales/en-US.json	2026-07-22 23:31:31.223459500 -0700
+++ external-player-launcher/assets/locales/en-US.json	2026-07-23 23:54:32.174399300 -0700
@@ -3,6 +3,8 @@
   "settings.modal.title": "External Player Settings",
   "settings.noteBold": "Note: ",
   "settings.noteText": "All settings only affect this client",
+  "settings.enableSceneTab": "Enable Players tab in a scene",
+  "settings.enableSceneTabHint": "When enabled, scene page shows a tab with settings and enabled players. When disabled, only shows the playback button (similar to grid view).",
   "settings.singlePlayerMode": "Single player mode",
   "settings.singlePlayerModeHint": "When turned on, the player selector at the bottom of scene cards will be replaced with the player button selected below.",
   "settings.playerSelection": "Player selection",
diff -urpN external-player-launcher.bak/main.js external-player-launcher/main.js
--- external-player-launcher.bak/main.js	2026-07-22 23:31:31.225459500 -0700
+++ external-player-launcher/main.js	2026-07-25 23:17:59.023308000 -0700
@@ -90,13 +90,15 @@
   const defaultSettings = {
     excludedPlayerIds: [],
     singlePlayerId: playerButtons[0].id,
-    singlePlayerMode: false
+    singlePlayerMode: false,
+    enableSceneTab: false
   };
   function cloneSettings(settings) {
     return {
       excludedPlayerIds: [...settings.excludedPlayerIds],
       singlePlayerId: settings.singlePlayerId,
-      singlePlayerMode: settings.singlePlayerMode
+      singlePlayerMode: settings.singlePlayerMode,
+      enableSceneTab: settings.enableSceneTab
     };
   }
   function normalizeSettings(parsed) {
@@ -106,7 +108,8 @@
     return {
       excludedPlayerIds,
       singlePlayerId,
-      singlePlayerMode: Boolean(parsed?.singlePlayerMode)
+      singlePlayerMode: Boolean(parsed?.singlePlayerMode),
+      enableSceneTab: Boolean(parsed?.enableSceneTab)
     };
   }
   function readSettings() {
@@ -402,7 +405,21 @@
         contentClassName: "external-player-settings-modal-content"
       },
       /* @__PURE__ */ React.createElement(Modal.Header, { closeButton: true }, /* @__PURE__ */ React.createElement(Modal.Title, null, /* @__PURE__ */ React.createElement(FormattedMessage, { id: "settings.modal.title" }))),
-      /* @__PURE__ */ React.createElement(Modal.Body, null, /* @__PURE__ */ React.createElement("div", { className: "note-block" }, "\u26A0\uFE0F", /* @__PURE__ */ React.createElement("strong", null, /* @__PURE__ */ React.createElement(FormattedMessage, { id: "settings.noteBold" })), /* @__PURE__ */ React.createElement(FormattedMessage, { id: "settings.noteText" })), /* @__PURE__ */ React.createElement("div", { className: "external-player-settings-section" }, /* @__PURE__ */ React.createElement(
+      /* @__PURE__ */ React.createElement(Modal.Body, null, /* @__PURE__ */ React.createElement("div", { className: "note-block" }, "\u26A0\uFE0F", /* @__PURE__ */ React.createElement("strong", null, /* @__PURE__ */ React.createElement(FormattedMessage, { id: "settings.noteBold" })), /* @__PURE__ */ React.createElement(FormattedMessage, { id: "settings.noteText" })),
+      /* @__PURE__ */ React.createElement("div", { className: "external-player-settings-section" },/* @__PURE__ */ React.createElement(
+        Form.Check,
+        {
+          type: "switch",
+          id: "external-player-scene-tab",
+          label: intl.formatMessage({ id: "settings.enableSceneTab" }),
+          checked: draftSettings.enableSceneTab,
+          onChange: (event) => setDraftSettings((current) => ({
+            ...current,
+            enableSceneTab: event.target.checked
+          }))
+        }
+      ), /* @__PURE__ */ React.createElement("div", { className: "external-player-settings-hint" }, /* @__PURE__ */ React.createElement(FormattedMessage, { id: "settings.enableSceneTabHint" }))),
+      /* @__PURE__ */ React.createElement("div", { className: "external-player-settings-section" },/* @__PURE__ */ React.createElement(
         Form.Check,
         {
           type: "switch",
@@ -466,7 +483,7 @@
       )
     ));
   }
-  function SceneCardExternalPlayerControls({ sceneProps }) {
+  function ExternalPlayerPlaybackControls({ sceneProps }) {
     const { settings } = useSettingsState();
     const [isOpen, setIsOpen] = React.useState(false);
     if (settings.singlePlayerMode) {
@@ -548,18 +565,29 @@
   PluginApi.patch.after(
     "ScenePage.Tabs",
     function(props, _, original) {
-      original.props.children.push(
-        /* @__PURE__ */ React.createElement(ExternalPlayerTabLabel, null)
-      );
+      const { settings } = useSettingsState();
+      if (settings.enableSceneTab) {
+        original.props.children.push(
+          /* @__PURE__ */ React.createElement(ExternalPlayerTabLabel, null)
+        );
+      }
+      else {
+        original.props.children.push(
+          /* @__PURE__ */ React.createElement(ExternalPlayerPlaybackControls, { key: "external-player-controls", sceneProps: props })
+        );
+      }
       return original;
     }
   );
   PluginApi.patch.after(
     "ScenePage.TabContent",
     function(props, _, original) {
-      original.props.children.push(
-        /* @__PURE__ */ React.createElement(ExternalPlayerTabHeader, { sceneProps: props })
-      );
+      const { settings } = useSettingsState();
+      if (settings.enableSceneTab) {
+        original.props.children.push(
+          /* @__PURE__ */ React.createElement(ExternalPlayerTabHeader, { sceneProps: props })
+        );
+      }
       return original;
     }
   );
@@ -573,7 +601,7 @@
         original,
         (node) => node?.type instanceof Object && node.type?.displayName === "ButtonGroup",
         "appendChild",
-        /* @__PURE__ */ React.createElement(SceneCardExternalPlayerControls, { key: "external-player-controls", sceneProps: props })
+        /* @__PURE__ */ React.createElement(ExternalPlayerPlaybackControls, { key: "external-player-controls", sceneProps: props })
       );
       return original;
     }
  • With the above change, I also tried to relocate the player launch button to view/O counters (which is where the “external player” button is located in the mobile UI), i.e. like this:

    But my React-fu is not strong enough :slight_smile: It apparently can be done (example) but seems to require looking up specific classes for elements, which would be fragile. I tried to play with injectIntoReactTree predicates to achieve this but did not succeed.

PS. I just updated to 1.1.0 and noticed there is an option to hide the tab from the scene details page. The patch above does not hide it but replaces it with a single-click launcher used by the scene card.

@Seneschal Adding a player button to the toolbar on the scene page (to the left of view/O counters) should be doable. The main issue is that Stash’s UI Plugin API doesn’t directly expose a “toolbar” component. Unlike ScenePage.Tabs and ScenePage.TabContent, which are PatchComponents and let me easily add tabs via the PluginApi.patch.after() function, there’s no ScenePage.ToolBar PatchComponent. Only ScenePage is a PatchComponent, so I’d need to find the “toolbar” element inside ScenePage and inject the player button component using the injectIntoReactTree function.

Maybe the approach in the “example” code is simpler, but I want to try my own way first.

Updated to Version 1.2.0

  • Added player buttons to the scene detail page toolbar
  • Added a setting to control the visibility of player buttons in the scene detail page toolbar
  • Saving settings on the scene detail page now automatically refreshes the current page so the display location settings take effect immediately
  • Refined UI strings and documentation for clearer, more consistent wording

@Seneschal I’ve added the player button to the toolbar, please update the plugin!:grin:

1 Like

That’s perfect now, thanks a lot!

Plugin looks awesome!

Could you add support for gridplayer please?

Thanks