使用 MPV 播放

通过 mpv-handler 播放网页上的视频和歌曲

  1. // ==UserScript==
  2. // @name Play with MPV -- enabled on all websites.
  3. // @name:en-US Play with MPV -- enabled on all websites.
  4. // @name:zh-CN 使用 MPV 播放
  5. // @name:zh-TW 使用 MPV 播放
  6. // @description Play videos and songs on the website via mpv-handler
  7. // @description:en-US Play videos and songs on the website via mpv-handler
  8. // @description:zh-CN 通过 mpv-handler 播放网页上的视频和歌曲
  9. // @description:zh-TW 通過 mpv-handler 播放網頁上的視頻和歌曲
  10. // @namespace play-with-mpv-handler
  11. // @version 2023.07.04
  12. // @author Akatsuki Rui, Stack Exploit
  13. // @license MIT License
  14. // @require https://cdn.jsdelivr.net/gh/sizzlemctwizzle/GM_config@2207c5c1322ebb56e401f03c2e581719f909762a/gm_config.js
  15. // @grant GM_info
  16. // @grant GM_getValue
  17. // @grant GM_setValue
  18. // @grant GM_notification
  19. // @run-at document-idle
  20. // @noframes
  21. // @include *
  22. // ==/UserScript==
  23.  
  24. "use strict";
  25.  
  26. const MPV_HANDLER_VERSION = "v0.3.4";
  27.  
  28. const MATCHERS = {
  29. "www.youtube.com": /www.youtube.com\/(watch|playlist|shorts)\?/gi,
  30. "m.youtube.com": /m.youtube.com\/(watch|playlist|shorts)\?/gi,
  31. "www.twitch.tv":
  32. /www.twitch.tv\/(?!(directory|downloads|jobs|p|turbo)\/).+/gi,
  33. "clips.twitch.tv": /clips.twitch.tv/gi,
  34. "www.crunchyroll.com": /www.crunchyroll.com\/.*watch\/([0-9]|[A-Z])+/gi,
  35. "beta.crunchyroll.com": /beta.crunchyroll.com\/.*watch\/([0-9]|[A-Z])+/gi,
  36. "live.bilibili.com": /live.bilibili.com\/[0-9]+/gi,
  37. "www.bilibili.com": /www.bilibili.com\/video\/(av|bv)/gi,
  38. "kick.com": /kick.com\/(?!(categories)\/).+/gi,
  39. };
  40.  
  41. const ICON_MPV =
  42. "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0\
  43. PSI2NCIgdmVyc2lvbj0iMSI+CiA8Y2lyY2xlIHN0eWxlPSJvcGFjaXR5Oi4yIiBjeD0iMzIiIGN5\
  44. PSIzMyIgcj0iMjgiLz4KIDxjaXJjbGUgc3R5bGU9ImZpbGw6IzhkMzQ4ZSIgY3g9IjMyIiBjeT0i\
  45. MzIiIHI9IjI4Ii8+CiA8Y2lyY2xlIHN0eWxlPSJvcGFjaXR5Oi4zIiBjeD0iMzQuNSIgY3k9IjI5\
  46. LjUiIHI9IjIwLjUiLz4KIDxjaXJjbGUgc3R5bGU9Im9wYWNpdHk6LjIiIGN4PSIzMiIgY3k9IjMz\
  47. IiByPSIxNCIvPgogPGNpcmNsZSBzdHlsZT0iZmlsbDojZmZmZmZmIiBjeD0iMzIiIGN5PSIzMiIg\
  48. cj0iMTQiLz4KIDxwYXRoIHN0eWxlPSJmaWxsOiM2OTFmNjkiIHRyYW5zZm9ybT0ibWF0cml4KDEu\
  49. NTE1NTQ0NSwwLDAsMS41LC0zLjY1Mzg3OSwtNC45ODczODQ4KSIgZD0ibTI3LjE1NDUxNyAyNC42\
  50. NTgyNTctMy40NjQxMDEgMi0zLjQ2NDEwMiAxLjk5OTk5OXYtNC0zLjk5OTk5OWwzLjQ2NDEwMiAy\
  51. eiIvPgogPHBhdGggc3R5bGU9ImZpbGw6I2ZmZmZmZjtvcGFjaXR5Oi4xIiBkPSJNIDMyIDQgQSAy\
  52. OCAyOCAwIDAgMCA0IDMyIEEgMjggMjggMCAwIDAgNC4wMjE0ODQ0IDMyLjU4NTkzOCBBIDI4IDI4\
  53. IDAgMCAxIDMyIDUgQSAyOCAyOCAwIDAgMSA1OS45Nzg1MTYgMzIuNDE0MDYyIEEgMjggMjggMCAw\
  54. IDAgNjAgMzIgQSAyOCAyOCAwIDAgMCAzMiA0IHoiLz4KPC9zdmc+Cg==";
  55.  
  56. const ICON_SETTINGS =
  57. "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0\
  58. PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KIDxkZWZzPgogIDxzdHlsZSBpZD0iY3VycmVudC1j\
  59. b2xvci1zY2hlbWUiIHR5cGU9InRleHQvY3NzIj4KICAgLkNvbG9yU2NoZW1lLVRleHQgeyBjb2xv\
  60. cjojNDQ0NDQ0OyB9IC5Db2xvclNjaGVtZS1IaWdobGlnaHQgeyBjb2xvcjojNDI4NWY0OyB9CiAg\
  61. PC9zdHlsZT4KIDwvZGVmcz4KIDxwYXRoIHN0eWxlPSJmaWxsOmN1cnJlbnRDb2xvciIgY2xhc3M9\
  62. IkNvbG9yU2NoZW1lLVRleHQiIGQ9Ik0gNi4yNSAxIEwgNi4wOTU3MDMxIDIuODQzNzUgQSA1LjUg\
  63. NS41IDAgMCAwIDQuNDg4MjgxMiAzLjc3MzQzNzUgTCAyLjgxMjUgMi45ODQzNzUgTCAxLjA2MjUg\
  64. Ni4wMTU2MjUgTCAyLjU4Mzk4NDQgNy4wNzIyNjU2IEEgNS41IDUuNSAwIDAgMCAyLjUgOCBBIDUu\
  65. NSA1LjUgMCAwIDAgMi41ODAwNzgxIDguOTMxNjQwNiBMIDEuMDYyNSA5Ljk4NDM3NSBMIDIuODEy\
  66. NSAxMy4wMTU2MjUgTCA0LjQ4NDM3NSAxMi4yMjg1MTYgQSA1LjUgNS41IDAgMCAwIDYuMDk1NzAz\
  67. MSAxMy4xNTIzNDQgTCA2LjI0NjA5MzggMTUuMDAxOTUzIEwgOS43NDYwOTM4IDE1LjAwMTk1MyBM\
  68. IDkuOTAwMzkwNiAxMy4xNTgyMDMgQSA1LjUgNS41IDAgMCAwIDExLjUwNzgxMiAxMi4yMjg1MTYg\
  69. TCAxMy4xODM1OTQgMTMuMDE3NTc4IEwgMTQuOTMzNTk0IDkuOTg2MzI4MSBMIDEzLjQxMjEwOSA4\
  70. LjkyOTY4NzUgQSA1LjUgNS41IDAgMCAwIDEzLjQ5NjA5NCA4LjAwMTk1MzEgQSA1LjUgNS41IDAg\
  71. MCAwIDEzLjQxNjAxNiA3LjA3MDMxMjUgTCAxNC45MzM1OTQgNi4wMTc1NzgxIEwgMTMuMTgzNTk0\
  72. IDIuOTg2MzI4MSBMIDExLjUxMTcxOSAzLjc3MzQzNzUgQSA1LjUgNS41IDAgMCAwIDkuOTAwMzkw\
  73. NiAyLjg0OTYwOTQgTCA5Ljc1IDEgTCA2LjI1IDEgeiBNIDggNiBBIDIgMiAwIDAgMSAxMCA4IEEg\
  74. MiAyIDAgMCAxIDggMTAgQSAyIDIgMCAwIDEgNiA4IEEgMiAyIDAgMCAxIDggNiB6IiB0cmFuc2Zv\
  75. cm09InRyYW5zbGF0ZSg0IDQpIi8+Cjwvc3ZnPgo=";
  76.  
  77. const MPV_CSS = `
  78. .pwm-play {
  79. width: 48px;
  80. height: 48px;
  81. border: 0;
  82. border-radius: 50%;
  83. background-size: 48px;
  84. background-image: url(data:image/svg+xml;base64,${ICON_MPV});
  85. background-repeat: no-repeat;
  86. }
  87. .pwm-settings {
  88. opacity: 50;
  89. visibility: hidden;
  90. transition: all 0.1s ease-in-out;
  91. display: block;
  92. position: absolute;
  93. top: -32px;
  94. width: 32px;
  95. height: 32px;
  96. margin-left: 8px;
  97. border: 0;
  98. border-radius: 50%;
  99. background-size: 32px;
  100. background-color: #eeeeee;
  101. background-image: url(data:image/svg+xml;base64,${ICON_SETTINGS});
  102. background-repeat: no-repeat;
  103. }
  104. .play-with-mpv {
  105. z-index: 99999;
  106. position: fixed;
  107. left: 8px;
  108. bottom: 8px;
  109. }
  110. .pwm-play:hover + .pwm-settings,
  111. .pwm-settings:hover {
  112. opacity: 1;
  113. visibility: visible;
  114. transition: all 0.2s ease-in-out;
  115. }
  116. `;
  117.  
  118. const CONFIG_ID = "play-with-mpv";
  119.  
  120. const CONFIG_CSS = `
  121. body {
  122. display: flex;
  123. justify-content: center;
  124. }
  125. #${CONFIG_ID}_wrapper {
  126. display: flex;
  127. flex-direction: column;
  128. justify-content: center;
  129. }
  130. #${CONFIG_ID} .config_header {
  131. display: flex;
  132. align-items: center;
  133. padding: 12px;
  134. }
  135. #${CONFIG_ID} .config_var {
  136. margin: 0 0 12px 0;
  137. }
  138. #${CONFIG_ID} .field_label {
  139. display: inline-block;
  140. width: 140px;
  141. font-size: 14px;
  142. }
  143. #${CONFIG_ID}_field_cookies,
  144. #${CONFIG_ID}_field_profile,
  145. #${CONFIG_ID}_field_quality,
  146. #${CONFIG_ID}_field_v_codec {
  147. width: 80px;
  148. height: 24px;
  149. font-size: 14px;
  150. text-align: center;
  151. }
  152. #${CONFIG_ID}_buttons_holder {
  153. display: flex;
  154. flex-direction: column;
  155. }
  156. #${CONFIG_ID} .saveclose_buttons {
  157. margin: 1px;
  158. padding: 4px 0;
  159. }
  160. #${CONFIG_ID} .reset_holder {
  161. padding-top: 4px;
  162. }
  163. `;
  164.  
  165. const CONFIG_IFRAME_CSS = `
  166. position: fixed;
  167. z-index: 99999;
  168. width: 300px;
  169. height: 300px;
  170. border: 1px solid;
  171. border-radius: 10px;
  172. `;
  173.  
  174. // GM_config init
  175. GM_config.init({
  176. id: `${CONFIG_ID}`,
  177. title: `${GM_info.script.name}`,
  178. fields: {
  179. cookies: {
  180. label: "Try Pass Cookies",
  181. type: "select",
  182. options: ["yes", "no"],
  183. default: "no",
  184. },
  185. profile: {
  186. label: "MPV Profile",
  187. type: "text",
  188. default: "default",
  189. },
  190. quality: {
  191. label: "Prefer Video Quality",
  192. type: "select",
  193. options: ["best", "2160p", "1440p", "1080p", "720p", "480p", "360p"],
  194. default: "best",
  195. },
  196. v_codec: {
  197. label: "Prefer Video Codec",
  198. type: "select",
  199. options: ["any", "av01", "vp9", "h265", "h264"],
  200. default: "any",
  201. },
  202. },
  203. events: {
  204. save: () => {
  205. let profile = GM_config.get("profile").trim();
  206.  
  207. if (profile === "") {
  208. GM_config.set("profile", "default");
  209. } else {
  210. GM_config.set("profile", profile);
  211. }
  212.  
  213. updateButton(location.href);
  214. GM_config.close();
  215. },
  216. reset: () => {
  217. GM_config.save();
  218. },
  219. },
  220. css: CONFIG_CSS.trim(),
  221. });
  222.  
  223. // URL-safe base64 encode
  224. function btoaUrl(url) {
  225. return btoa(url).replace(/\//g, "_").replace(/\+/g, "-").replace(/\=/g, "");
  226. }
  227.  
  228. // Generate "mpv://play/" protocol
  229. function generateProto(url) {
  230. let cookies = GM_config.get("cookies").toLowerCase();
  231. let profile = GM_config.get("profile").trim();
  232. let quality = GM_config.get("quality").toLowerCase();
  233. let v_codec = GM_config.get("v_codec").toLowerCase();
  234. let options = [];
  235.  
  236. let proto = "mpv://play/" + btoaUrl(url);
  237.  
  238. if (cookies === "yes") {
  239. options.push("cookies=" + document.location.hostname + ".txt");
  240. }
  241. if (profile !== "default" && profile !== "") {
  242. options.push("profile=" + profile);
  243. }
  244. if (quality !== "best") {
  245. options.push("quality=" + quality);
  246. }
  247. if (v_codec !== "any") {
  248. options.push("v_codec=" + v_codec);
  249. }
  250.  
  251. if (options.length !== 0) {
  252. proto += "/?";
  253.  
  254. options.forEach((option, index) => {
  255. proto += option;
  256.  
  257. if (index + 1 !== options.length) {
  258. proto += "&";
  259. }
  260. });
  261. }
  262.  
  263. return proto;
  264. }
  265.  
  266. // Check the URL is matched or not
  267. function matchUrl(url) {
  268. if (MATCHERS[location.hostname]) {
  269. return url.search(MATCHERS[location.hostname]) !== -1;
  270. } else {
  271. return url.search(MATCHERS[location.hostname]) !== -1;
  272. }
  273. }
  274.  
  275. // Update button display status and URL
  276. function updateButton(url) {
  277. let isMatch = matchUrl(url);
  278. let button = document.getElementsByClassName("pwm-play")[0];
  279.  
  280. if (button) {
  281. button.style =
  282. isMatch && !document.fullscreenElement
  283. ? "display: block"
  284. : "display: none";
  285. button.href = isMatch ? generateProto(url) : "";
  286. }
  287. }
  288.  
  289. // Notify update about mpv-handler
  290. function notifyUpdate() {
  291. let version = GM_getValue("mpvHandlerVersion", null);
  292.  
  293. if (version !== MPV_HANDLER_VERSION) {
  294. const UPDATE_NOTIFY = {
  295. title: `${GM_info.script.name}`,
  296. text: `mpv-handler is upgraded to ${MPV_HANDLER_VERSION}\n\nClick to check updates`,
  297. onclick: () => {
  298. window.open("https://github.com/akiirui/mpv-handler/releases/latest");
  299. },
  300. };
  301.  
  302. GM_notification(UPDATE_NOTIFY);
  303. GM_setValue("mpvHandlerVersion", MPV_HANDLER_VERSION);
  304. }
  305. }
  306.  
  307. // Add play and settings buttons to page
  308. function createButton() {
  309. let head = document.getElementsByTagName("head")[0];
  310. let style = document.createElement("style");
  311.  
  312. if (head) {
  313. style.innerHTML = MPV_CSS.trim();
  314. head.appendChild(style);
  315. }
  316.  
  317. let body = document.body;
  318. let buttonDiv = document.createElement("div");
  319. let buttonPlay = document.createElement("a");
  320. let buttonSettings = document.createElement("button");
  321.  
  322. if (body) {
  323. buttonPlay.className = "pwm-play";
  324. buttonPlay.style = "display: none";
  325. buttonPlay.target = "_blank";
  326. buttonPlay.addEventListener("click", (e) => {
  327. let videoElement = document.getElementsByTagName("video")[0];
  328. if (videoElement) videoElement.pause();
  329. if (e.stopPropagation) e.stopPropagation();
  330. });
  331.  
  332. buttonSettings.className = "pwm-settings";
  333. buttonSettings.addEventListener("click", () => {
  334. if (!GM_config.isOpen) {
  335. GM_config.open();
  336. GM_config.frame.style = CONFIG_IFRAME_CSS.trim();
  337. }
  338. });
  339.  
  340. buttonDiv.className = "play-with-mpv";
  341. buttonDiv.appendChild(buttonPlay);
  342. buttonDiv.appendChild(buttonSettings);
  343.  
  344. body.appendChild(buttonDiv);
  345.  
  346. document.addEventListener("fullscreenchange", () => {
  347. let button = document.getElementsByClassName("pwm-play")[0];
  348.  
  349. button.style = document.fullscreenElement
  350. ? "display: none"
  351. : "display: block";
  352. });
  353. }
  354. }
  355.  
  356. // Detect PJAX changes
  357. function detectPJAX() {
  358. let previousUrl = null;
  359. let currentUrl = null;
  360.  
  361. setInterval(() => {
  362. currentUrl = location.href;
  363.  
  364. if (previousUrl !== currentUrl) {
  365. updateButton(currentUrl);
  366. previousUrl = currentUrl;
  367. }
  368. }, 500);
  369. }
  370.  
  371. notifyUpdate();
  372. createButton();
  373. detectPJAX();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址