Last active
February 10, 2026 20:22
-
-
Save eplord/964175a6f8f3a2c3d0b3fa9eb10c7262 to your computer and use it in GitHub Desktop.
Mega Infinite - Audio/Video Enhancement Script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Mega Infinite - Audio/Video Enhancement Script | |
| // @namespace https://gist.githubusercontent.com/eplord/964175a6f8f3a2c3d0b3fa9eb10c7262/raw | |
| // @homepage https://gist.githubusercontent.com/eplord/964175a6f8f3a2c3d0b3fa9eb10c7262/raw | |
| // @version 4.4.0 | |
| // @description HTML5 video enhancement for all sites. Supports: speed control, screenshots, PiP, fullscreen, brightness/saturation/contrast adjustments, and more. | |
| // @icon data:image/png;base64,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 | |
| // @match *://*/* | |
| // @exclude *://yiyan.baidu.com/* | |
| // @exclude *://*.bing.com/search* | |
| // @grant unsafeWindow | |
| // @grant GM_addStyle | |
| // @grant GM_setValue | |
| // @grant GM_getValue | |
| // @grant GM_deleteValue | |
| // @grant GM_listValues | |
| // @grant GM_addValueChangeListener | |
| // @grant GM_removeValueChangeListener | |
| // @grant GM_registerMenuCommand | |
| // @grant GM_unregisterMenuCommand | |
| // @grant GM_getTab | |
| // @grant GM_saveTab | |
| // @grant GM_getTabs | |
| // @grant GM_openInTab | |
| // @grant GM_setClipboard | |
| // @run-at document-start | |
| // @license GPL | |
| // ==/UserScript== | |
| /** | |
| * H5Player - HTML5 Audio/Video Enhancement Script | |
| * | |
| * @version 4.4.0 | |
| * @author xxxily | |
| * @homepage https://github.com/xxxily/h5player | |
| * | |
| * Features: | |
| * - Unlimited playback speed control | |
| * - Video screenshots | |
| * - Picture-in-Picture mode | |
| * - Web fullscreen | |
| * - Brightness/Contrast/Saturation adjustments | |
| * - Mirror flip | |
| * - Frame-by-frame navigation | |
| * - Keyboard shortcuts | |
| * - Multi-language support | |
| */ | |
| (function () { | |
| "use strict"; | |
| // Set window name for identification | |
| if (window) { | |
| window.name = "h5player"; | |
| } | |
| /* ============================================================================ | |
| * SECTION 1: Original Methods Preservation | |
| * Save important original functions to prevent contamination by external scripts | |
| * ============================================================================ */ | |
| const originalMethods = { | |
| Object: { | |
| defineProperty: Object.defineProperty, | |
| defineProperties: Object.defineProperties, | |
| getOwnPropertyDescriptor: Object.getOwnPropertyDescriptor, | |
| getOwnPropertyDescriptors: Object.getOwnPropertyDescriptors, | |
| }, | |
| setInterval: window.setInterval, | |
| setTimeout: window.setTimeout, | |
| clearInterval: window.clearInterval, | |
| clearTimeout: window.clearTimeout, | |
| HTMLElement: window.HTMLElement, | |
| customElements: window.customElements, | |
| customElementsMethods: { | |
| define: window.customElements.define, | |
| get: window.customElements.get, | |
| }, | |
| }; | |
| /* ============================================================================ | |
| * SECTION 2: Original Prototypes | |
| * Store native functions to prevent external contamination | |
| * ============================================================================ */ | |
| const original = { | |
| Object: { | |
| defineProperty: Object.defineProperty, | |
| defineProperties: Object.defineProperties, | |
| }, | |
| Proxy, | |
| Map, | |
| map: { | |
| clear: Map.prototype.clear, | |
| set: Map.prototype.set, | |
| has: Map.prototype.has, | |
| get: Map.prototype.get, | |
| delete: Map.prototype.delete, | |
| }, | |
| console: { | |
| log: console.log, | |
| info: console.info, | |
| error: console.error, | |
| warn: console.warn, | |
| table: console.table, | |
| }, | |
| ShadowRoot, | |
| HTMLMediaElement, | |
| CustomEvent, | |
| JSON: { | |
| parse: JSON.parse, | |
| stringify: JSON.stringify, | |
| }, | |
| alert, | |
| confirm, | |
| prompt, | |
| }; | |
| /* ============================================================================ | |
| * SECTION 3: DOM Utilities | |
| * Element listener, Shadow DOM support | |
| * ============================================================================ */ | |
| /** | |
| * Element ready listener using MutationObserver | |
| * @param {string|string[]} selector - CSS selector(s) to watch for | |
| * @param {Function} fn - Callback when element exists | |
| * @param {ShadowRoot} [shadowRoot] - Optional shadowRoot to observe | |
| */ | |
| function ready(selector, fn, shadowRoot) { | |
| const win = window; | |
| const docRoot = shadowRoot || win.document.documentElement; | |
| if (!docRoot) return false; | |
| const MutationObserver = win.MutationObserver || win.WebKitMutationObserver; | |
| const listeners = docRoot._MutationListeners || []; | |
| function $ready(selector, fn) { | |
| listeners.push({ selector, fn }); | |
| if (!docRoot._MutationListeners || !docRoot._MutationObserver) { | |
| docRoot._MutationListeners = listeners; | |
| docRoot._MutationObserver = new MutationObserver(() => { | |
| for (let i = 0; i < docRoot._MutationListeners.length; i++) { | |
| const item = docRoot._MutationListeners[i]; | |
| check(item.selector, item.fn); | |
| } | |
| }); | |
| docRoot._MutationObserver.observe(docRoot, { | |
| childList: true, | |
| subtree: true, | |
| }); | |
| } | |
| check(selector, fn); | |
| } | |
| function check(selector, fn) { | |
| const elements = docRoot.querySelectorAll(selector); | |
| for (let i = 0; i < elements.length; i++) { | |
| const element = elements[i]; | |
| element._MutationReadyList_ = element._MutationReadyList_ || []; | |
| if (!element._MutationReadyList_.includes(fn)) { | |
| element._MutationReadyList_.push(fn); | |
| fn.call(element, element); | |
| } | |
| } | |
| } | |
| const selectorArr = Array.isArray(selector) ? selector : [selector]; | |
| selectorArr.forEach((sel) => $ready(sel, fn)); | |
| } | |
| /** | |
| * Hack attachShadow to intercept closed shadow roots | |
| * Required for sites like Baidu Cloud that use closed shadow DOM | |
| */ | |
| function hackAttachShadow() { | |
| if (window._hasHackAttachShadow_) return; | |
| try { | |
| window._shadowDomList_ = []; | |
| window.Element.prototype._attachShadow = | |
| window.Element.prototype.attachShadow; | |
| window.Element.prototype.attachShadow = function (options) { | |
| const isClosed = options && options.mode === "closed"; | |
| if (options && options.mode) { | |
| options.mode = "open"; | |
| } | |
| const shadowRoot = this._attachShadow.apply(this, arguments); | |
| window._shadowDomList_.push(shadowRoot); | |
| shadowRoot._shadowHost = this; | |
| const shadowEvent = new window.CustomEvent("addShadowRoot", { | |
| shadowRoot, | |
| detail: { shadowRoot, message: "addShadowRoot", time: new Date() }, | |
| bubbles: true, | |
| cancelable: true, | |
| }); | |
| document.dispatchEvent(shadowEvent); | |
| if (isClosed) { | |
| Object.defineProperty(this, "shadowRoot", { | |
| get() { | |
| return null; | |
| }, | |
| }); | |
| } | |
| return shadowRoot; | |
| }; | |
| window._hasHackAttachShadow_ = true; | |
| } catch (e) { | |
| console.error("[H5Player] hackAttachShadow error:", e); | |
| } | |
| } | |
| /* ============================================================================ | |
| * SECTION 4: Utility Functions | |
| * Type detection, object operations, DOM helpers | |
| * ============================================================================ */ | |
| /** | |
| * Get precise type of any value | |
| * @param {*} obj - Value to check | |
| * @returns {string} Lowercase type name | |
| */ | |
| function getType(obj) { | |
| if (obj == null) return String(obj); | |
| return typeof obj === "object" || typeof obj === "function" | |
| ? (obj.constructor && | |
| obj.constructor.name && | |
| obj.constructor.name.toLowerCase()) || | |
| /function\s(.+?)\(/.exec(obj.constructor)[1].toLowerCase() | |
| : typeof obj; | |
| } | |
| const isType = (obj, typeName) => getType(obj) === typeName; | |
| const isObj = (obj) => isType(obj, "object"); | |
| const isFunc = (obj) => typeof obj === "function"; | |
| const isArr = (arr) => Array.isArray(arr); | |
| const isStr = (str) => typeof str === "string"; | |
| const isNum = (num) => typeof num === "number" && !Number.isNaN(num); | |
| /** | |
| * Deep clone an object | |
| * @param {Object|Array} source - Object to clone | |
| * @returns {Object|Array} Cloned object | |
| */ | |
| function clone(source) { | |
| if (typeof source !== "object" || source === null) return source; | |
| if (Array.isArray(source)) { | |
| return source.map((item) => clone(item)); | |
| } | |
| const result = {}; | |
| for (const key in source) { | |
| if (Object.prototype.hasOwnProperty.call(source, key)) { | |
| result[key] = clone(source[key]); | |
| } | |
| } | |
| return result; | |
| } | |
| /** | |
| * Iterate object properties (excluding prototype chain) | |
| * @param {Object} obj - Object to iterate | |
| * @param {Function} fn - Callback(key, value) | |
| */ | |
| function forIn(obj, fn) { | |
| fn = fn || function () {}; | |
| for (const key in obj) { | |
| if (Object.prototype.hasOwnProperty.call(obj, key)) { | |
| fn(key, obj[key]); | |
| } | |
| } | |
| } | |
| /** | |
| * Deep merge two objects | |
| * @param {Object} objA - Target object | |
| * @param {Object} objB - Source object | |
| * @param {boolean} [concatArr] - Concatenate arrays instead of replacing | |
| * @returns {Object} Merged object | |
| */ | |
| function mergeObj(objA, objB, concatArr) { | |
| if (!isObj(objA) || !isObj(objB)) return objA; | |
| function deepMerge(a, b) { | |
| forIn(b, function (key) { | |
| const subA = a[key]; | |
| const subB = b[key]; | |
| if (typeof subA === "undefined") { | |
| a[key] = subB; | |
| } else if (isObj(subA) && isObj(subB)) { | |
| a[key] = deepMerge(subA, subB); | |
| } else if (concatArr && isArr(subA) && isArr(subB)) { | |
| a[key] = subA.concat(subB); | |
| } else { | |
| a[key] = subB; | |
| } | |
| }); | |
| return a; | |
| } | |
| return deepMerge(objA, objB); | |
| } | |
| /** | |
| * Get value from object by path string | |
| * @param {Object} obj - Object to query | |
| * @param {string} path - Dot-separated path | |
| * @returns {*} Value at path | |
| */ | |
| function getValByPath(obj, path) { | |
| if (!path) return obj; | |
| const pathArr = path.split("."); | |
| let result = obj; | |
| for (let i = 0; i < pathArr.length; i++) { | |
| if (!result) break; | |
| result = result[pathArr[i]]; | |
| } | |
| return result; | |
| } | |
| /** | |
| * Set value in object by path string | |
| * @param {Object} obj - Object to modify | |
| * @param {string} path - Dot-separated path | |
| * @param {*} val - Value to set | |
| * @returns {boolean} Success status | |
| */ | |
| function setValByPath(obj, path, val) { | |
| if (!obj || !path || typeof path !== "string") return false; | |
| const pathArr = path.split("."); | |
| let result = obj; | |
| for (let i = 0; i < pathArr.length; i++) { | |
| if (!result) break; | |
| if (i === pathArr.length - 1) { | |
| result[pathArr[i]] = val; | |
| return Number.isNaN(val) | |
| ? Number.isNaN(result[pathArr[i]]) | |
| : result[pathArr[i]] === val; | |
| } | |
| result = result[pathArr[i]]; | |
| } | |
| return false; | |
| } | |
| /** | |
| * Throttle function execution | |
| * @param {Function} fn - Function to throttle | |
| * @param {number} [interval=80] - Throttle interval in ms | |
| * @returns {Function} Throttled function | |
| */ | |
| function throttle(fn, interval = 80) { | |
| let timeout = null; | |
| return function (...args) { | |
| if (timeout) return false; | |
| timeout = setTimeout(() => { | |
| timeout = null; | |
| }, interval); | |
| fn.apply(this, args); | |
| }; | |
| } | |
| /** | |
| * Debounce function execution | |
| * @param {Function} fn - Function to debounce | |
| * @param {number} [delay=100] - Debounce delay in ms | |
| * @returns {Function} Debounced function | |
| */ | |
| function debounce(fn, delay = 100) { | |
| let timeout = null; | |
| return function (...args) { | |
| if (timeout) clearTimeout(timeout); | |
| timeout = setTimeout(() => fn.apply(this, args), delay); | |
| }; | |
| } | |
| /* DOM Utilities */ | |
| function hideDom(selector, delay = 5000) { | |
| setTimeout(() => { | |
| const dom = document.querySelector(selector); | |
| if (dom) dom.style.opacity = 0; | |
| }, delay); | |
| } | |
| function eachParentNode(dom, fn) { | |
| let parent = dom.parentNode; | |
| while (parent) { | |
| if (fn(parent, dom)) break; | |
| parent = parent.parentNode; | |
| } | |
| } | |
| function loadCSSText(cssText, id, insertTo) { | |
| if (id && document.getElementById(id)) return false; | |
| const style = document.createElement("style"); | |
| const head = | |
| insertTo || document.head || document.getElementsByTagName("head")[0]; | |
| style.appendChild(document.createTextNode(cssText)); | |
| head.appendChild(style); | |
| if (id) style.setAttribute("id", id); | |
| return style; | |
| } | |
| function isEditableTarget(target) { | |
| const isEditable = | |
| target.getAttribute && target.getAttribute("contenteditable") === "true"; | |
| const isInputDom = /INPUT|TEXTAREA|SELECT|LABEL/.test(target.nodeName); | |
| return isEditable || isInputDom; | |
| } | |
| function isInShadow(node, returnShadowRoot) { | |
| for (; node; node = node.parentNode) { | |
| if (node.toString() === "[object ShadowRoot]") { | |
| return returnShadowRoot ? node : true; | |
| } | |
| } | |
| return false; | |
| } | |
| function isInViewPort(element) { | |
| const viewWidth = window.innerWidth || document.documentElement.clientWidth; | |
| const viewHeight = | |
| window.innerHeight || document.documentElement.clientHeight; | |
| const { top, right, bottom, left } = element.getBoundingClientRect(); | |
| return top >= 0 && left >= 0 && right <= viewWidth && bottom <= viewHeight; | |
| } | |
| function isOutOfDocument(element) { | |
| if (!element || element.offsetParent === null) return true; | |
| if ( | |
| element.style.visibility === "hidden" || | |
| element.style.display === "none" | |
| ) | |
| return true; | |
| const { top, right, bottom, left, width, height } = | |
| element.getBoundingClientRect(); | |
| return ( | |
| top === 0 && | |
| right === 0 && | |
| bottom === 0 && | |
| left === 0 && | |
| width === 0 && | |
| height === 0 | |
| ); | |
| } | |
| function isCoordinateInElement(x, y, element) { | |
| if (!element || !element.getBoundingClientRect) return false; | |
| const rect = element.getBoundingClientRect(); | |
| return ( | |
| x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom | |
| ); | |
| } | |
| function createTrustedHTML(htmlString) { | |
| if (window.trustedTypes && window.trustedTypes.createPolicy) { | |
| let policy = window.trustedTypes.defaultPolicy || null; | |
| if (!policy) { | |
| policy = window.trustedTypes.createPolicy("default", { | |
| createHTML: (string) => string, | |
| }); | |
| } | |
| return policy.createHTML(htmlString); | |
| } | |
| return htmlString; | |
| } | |
| function parseHTML(htmlString, targetElement) { | |
| if (typeof htmlString !== "string") { | |
| throw new Error("[parseHTML] Input must be a string"); | |
| } | |
| const trustedHTML = createTrustedHTML(htmlString); | |
| const parser = new DOMParser(); | |
| const doc = parser.parseFromString(trustedHTML, "text/html"); | |
| const nodes = doc.body.childNodes; | |
| const result = []; | |
| if (targetElement && targetElement.appendChild) { | |
| nodes.forEach((node) => { | |
| const targetNode = node.cloneNode(true); | |
| try { | |
| targetElement.appendChild(targetNode); | |
| result.push(targetNode); | |
| } catch (e) { | |
| console.error("[parseHTML] appendChild error", e); | |
| } | |
| }); | |
| } | |
| return result.length ? result : nodes; | |
| } | |
| function inlineStyleToObj(inlineStyle) { | |
| if (typeof inlineStyle !== "string") return {}; | |
| const result = {}; | |
| inlineStyle.split(";").forEach((item) => { | |
| const [key, val] = item.split(":"); | |
| if (key && val) { | |
| result[key.trim()] = val.trim(); | |
| } | |
| }); | |
| return result; | |
| } | |
| function objToInlineStyle(obj) { | |
| if (!isObj(obj)) return ""; | |
| return Object.entries(obj) | |
| .map(([k, v]) => `${k}: ${v}`) | |
| .join("; "); | |
| } | |
| function isInIframe() { | |
| return window !== window.top; | |
| } | |
| function isInCrossOriginFrame() { | |
| try { | |
| return !(window.top.localStorage || window.top.location.href); | |
| } catch (e) { | |
| return true; | |
| } | |
| } | |
| /* ============================================================================ | |
| * SECTION 5: URL Utilities | |
| * URL parsing and manipulation | |
| * ============================================================================ */ | |
| /** | |
| * Parse URL into components | |
| * @param {string} [url] - URL to parse (defaults to current page) | |
| * @returns {Object} Parsed URL components | |
| */ | |
| function parseURL(url) { | |
| const a = document.createElement("a"); | |
| a.href = url || window.location.href; | |
| return { | |
| source: url, | |
| protocol: a.protocol.replace(":", ""), | |
| host: a.hostname, | |
| port: a.port, | |
| origin: a.origin, | |
| search: a.search, | |
| query: a.search, | |
| file: (a.pathname.match(/\/([^/?#]+)$/i) || ["", ""])[1], | |
| hash: a.hash.replace("#", ""), | |
| path: a.pathname.replace(/^([^/])/, "/$1"), | |
| relative: (a.href.match(/tps?:\/\/[^/]+(.+)/) || ["", ""])[1], | |
| params: (function () { | |
| const ret = {}; | |
| const paramArr = a.search.replace(/^\?/, "").split("&"); | |
| paramArr.forEach((item) => { | |
| if (item && item.includes("=")) { | |
| const idx = item.indexOf("="); | |
| const key = item.substring(0, idx); | |
| const val = item.substring(idx + 1); | |
| if (key) { | |
| ret[key] = val; | |
| } | |
| } | |
| }); | |
| return ret; | |
| })(), | |
| }; | |
| } | |
| function stringifyParams(params) { | |
| if (!isObj(params)) return ""; | |
| const strArr = []; | |
| for (const key in params) { | |
| if (Object.prototype.hasOwnProperty.call(params, key)) { | |
| const val = params[key]; | |
| const valType = Object.prototype.toString.call(val); | |
| if (val === "" || valType === "[object Undefined]") continue; | |
| if (val === null) { | |
| strArr.push(key); | |
| } else if (valType === "[object Array]") { | |
| strArr.push(key + "=" + val.join(",")); | |
| } else { | |
| const strVal = (JSON.stringify(val) || String(val)).replace( | |
| /(^"|"$)/g, | |
| "", | |
| ); | |
| strArr.push(key + "=" + strVal); | |
| } | |
| } | |
| } | |
| return strArr.join("&"); | |
| } | |
| function stringifyToUrl(urlObj) { | |
| const query = stringifyParams(urlObj.params) || ""; | |
| const queryStr = query ? "?" + query : ""; | |
| const hash = urlObj.hash ? "#" + urlObj.hash : ""; | |
| return urlObj.origin + urlObj.path + queryStr + hash; | |
| } | |
| /* ============================================================================ | |
| * SECTION 6: Media Core | |
| * HTMLMediaElement proxy and enhancement API | |
| * ============================================================================ */ | |
| const mediaCore = (function () { | |
| let hasMediaCoreInit = false; | |
| let hasProxyHTMLMediaElement = false; | |
| let originDescriptors = {}; | |
| const originMethods = {}; | |
| const mediaElementList = []; | |
| const mediaElementHandler = []; | |
| const mediaMap = new original.Map(); | |
| const firstUpperCase = (str) => str.replace(/^\S/, (s) => s.toUpperCase()); | |
| function isHTMLMediaElement(el) { | |
| return el instanceof original.HTMLMediaElement; | |
| } | |
| /** | |
| * Create enhanced API for media element | |
| * Allows locking playbackRate, volume, currentTime, play, pause | |
| */ | |
| function createMediaPlusApi(mediaElement) { | |
| if (!isHTMLMediaElement(mediaElement)) return false; | |
| let mediaPlusApi = original.map.get.call(mediaMap, mediaElement); | |
| if (mediaPlusApi) return mediaPlusApi; | |
| mediaPlusApi = {}; | |
| const mediaPlusBaseApi = { | |
| lock(keyName, duration) { | |
| const infoKey = `__${keyName}_info__`; | |
| mediaPlusApi[infoKey] = mediaPlusApi[infoKey] || {}; | |
| mediaPlusApi[infoKey].lock = true; | |
| duration = Number(duration); | |
| if (!Number.isNaN(duration) && duration > 0) { | |
| mediaPlusApi[infoKey].unLockTime = Date.now() + duration; | |
| } | |
| }, | |
| unLock(keyName) { | |
| const infoKey = `__${keyName}_info__`; | |
| mediaPlusApi[infoKey] = mediaPlusApi[infoKey] || {}; | |
| mediaPlusApi[infoKey].lock = false; | |
| mediaPlusApi[infoKey].unLockTime = Date.now() - 100; | |
| }, | |
| isLock(keyName) { | |
| const info = mediaPlusApi[`__${keyName}_info__`] || {}; | |
| if (info.unLockTime) { | |
| return Date.now() < info.unLockTime; | |
| } | |
| return info.lock || false; | |
| }, | |
| get(keyName) { | |
| if (originDescriptors[keyName]?.get && !originMethods[keyName]) { | |
| return originDescriptors[keyName].get.apply(mediaElement); | |
| } | |
| }, | |
| set(keyName, val) { | |
| if ( | |
| originDescriptors[keyName]?.set && | |
| !originMethods[keyName] && | |
| val !== undefined | |
| ) { | |
| return originDescriptors[keyName].set.apply(mediaElement, [val]); | |
| } | |
| }, | |
| apply(keyName, ...args) { | |
| if (originMethods[keyName] instanceof Function) { | |
| return originMethods[keyName].apply(mediaElement, args); | |
| } | |
| }, | |
| }; | |
| mediaPlusApi = { ...mediaPlusApi, ...mediaPlusBaseApi }; | |
| // Create convenience methods for common properties | |
| const extApiKeys = [ | |
| "playbackRate", | |
| "volume", | |
| "currentTime", | |
| "play", | |
| "pause", | |
| ]; | |
| const baseApiKeys = Object.keys(mediaPlusBaseApi); | |
| extApiKeys.forEach((key) => { | |
| baseApiKeys.forEach((baseKey) => { | |
| if (originMethods[key] instanceof Function) { | |
| if (baseKey === "get" || baseKey === "set") return; | |
| } else if (baseKey === "apply") { | |
| return; | |
| } | |
| mediaPlusApi[`${baseKey}${firstUpperCase(key)}`] = function ( | |
| ...args | |
| ) { | |
| return mediaPlusBaseApi[baseKey].apply(null, [key, ...args]); | |
| }; | |
| }); | |
| }); | |
| original.map.set.call(mediaMap, mediaElement, mediaPlusApi); | |
| return mediaPlusApi; | |
| } | |
| function mediaDetectHandler(ctx) { | |
| if (isHTMLMediaElement(ctx) && !mediaElementList.includes(ctx)) { | |
| mediaElementList.push(ctx); | |
| createMediaPlusApi(ctx); | |
| try { | |
| mediaElementHandler.forEach((handler) => { | |
| if (handler instanceof Function) handler(ctx); | |
| }); | |
| } catch (e) {} | |
| } | |
| } | |
| function proxyPrototypeMethod(element, methodName) { | |
| const originFunc = element?.prototype[methodName]; | |
| if (!originFunc) return; | |
| element.prototype[methodName] = new original.Proxy(originFunc, { | |
| apply(target, ctx, args) { | |
| mediaDetectHandler(ctx); | |
| if (["play", "pause"].includes(methodName)) { | |
| const mediaPlusApi = createMediaPlusApi(ctx); | |
| if (mediaPlusApi?.isLock(methodName)) return; | |
| } | |
| return target.apply(ctx, args); | |
| }, | |
| }); | |
| } | |
| function hijackPrototypeProperty(element, property) { | |
| if (!element?.prototype || !originDescriptors[property]) return false; | |
| original.Object.defineProperty.call(Object, element.prototype, property, { | |
| configurable: true, | |
| enumerable: true, | |
| get: function () { | |
| const val = originDescriptors[property].get.apply(this, arguments); | |
| const mediaPlusApi = createMediaPlusApi(this); | |
| if (mediaPlusApi?.isLock(property)) { | |
| if (property === "playbackRate") return 1; | |
| } | |
| return val; | |
| }, | |
| set: function (value) { | |
| if (property === "src") { | |
| mediaDetectHandler(this); | |
| } | |
| if (["playbackRate", "volume", "currentTime"].includes(property)) { | |
| const mediaPlusApi = createMediaPlusApi(this); | |
| if (mediaPlusApi?.isLock(property)) return; | |
| } | |
| return originDescriptors[property].set.apply(this, arguments); | |
| }, | |
| }); | |
| } | |
| function mediaPlus(mediaElement) { | |
| return createMediaPlusApi(mediaElement); | |
| } | |
| function mediaProxy() { | |
| if (hasProxyHTMLMediaElement) return true; | |
| const proxyMethods = ["play", "pause", "load", "addEventListener"]; | |
| proxyMethods.forEach((methodName) => | |
| proxyPrototypeMethod(HTMLMediaElement, methodName), | |
| ); | |
| const hijackProperties = ["playbackRate", "volume", "currentTime", "src"]; | |
| hijackProperties.forEach((property) => | |
| hijackPrototypeProperty(HTMLMediaElement, property), | |
| ); | |
| hasProxyHTMLMediaElement = true; | |
| return true; | |
| } | |
| function mediaChecker(handler) { | |
| if ( | |
| !(handler instanceof Function) || | |
| mediaElementHandler.includes(handler) | |
| ) { | |
| return mediaElementList; | |
| } | |
| mediaElementHandler.push(handler); | |
| if (!hasProxyHTMLMediaElement) mediaProxy(); | |
| return mediaElementList; | |
| } | |
| function init(mediaCheckerHandler) { | |
| if (hasMediaCoreInit) return false; | |
| originDescriptors = Object.getOwnPropertyDescriptors( | |
| HTMLMediaElement.prototype, | |
| ); | |
| Object.keys(HTMLMediaElement.prototype).forEach((key) => { | |
| try { | |
| if (HTMLMediaElement.prototype[key] instanceof Function) { | |
| originMethods[key] = HTMLMediaElement.prototype[key]; | |
| } | |
| } catch (e) {} | |
| }); | |
| mediaCheckerHandler = | |
| mediaCheckerHandler instanceof Function | |
| ? mediaCheckerHandler | |
| : function () {}; | |
| mediaChecker(mediaCheckerHandler); | |
| hasMediaCoreInit = true; | |
| return true; | |
| } | |
| return { | |
| init, | |
| mediaPlus, | |
| mediaChecker, | |
| originDescriptors, | |
| originMethods, | |
| mediaElementList, | |
| }; | |
| })(); | |
| /* ============================================================================ | |
| * SECTION 7: Hotkey Configuration | |
| * Default keyboard shortcuts | |
| * ============================================================================ */ | |
| const defaultHotkeys = [ | |
| { desc: "Web fullscreen", key: "shift+enter", command: "setWebFullScreen" }, | |
| { desc: "Fullscreen", key: "enter", command: "setFullScreen" }, | |
| { | |
| desc: "Picture-in-Picture", | |
| key: "shift+p", | |
| command: "togglePictureInPicture", | |
| }, | |
| { desc: "Screenshot", key: "shift+s", command: "capture" }, | |
| { | |
| desc: "Toggle resume progress", | |
| key: "shift+r", | |
| command: "switchRestorePlayProgressStatus", | |
| }, | |
| { | |
| desc: "Vertical mirror", | |
| key: "shift+m", | |
| command: "setMirror", | |
| args: [true], | |
| }, | |
| { desc: "Horizontal mirror", key: "m", command: "setMirror" }, | |
| { desc: "Download media", key: "shift+d", command: "mediaDownload" }, | |
| { | |
| desc: "Scale down -0.05", | |
| key: "shift+x", | |
| command: "setScaleDown", | |
| args: -0.05, | |
| }, | |
| { | |
| desc: "Scale up +0.05", | |
| key: "shift+c", | |
| command: "setScaleUp", | |
| args: 0.05, | |
| }, | |
| { desc: "Reset transform", key: "shift+z", command: "resetTransform" }, | |
| { | |
| desc: "Move right 10px", | |
| key: "shift+arrowright", | |
| command: "setTranslateRight", | |
| args: 10, | |
| }, | |
| { | |
| desc: "Move left 10px", | |
| key: "shift+arrowleft", | |
| command: "setTranslateLeft", | |
| args: -10, | |
| }, | |
| { | |
| desc: "Move up 10px", | |
| key: "shift+arrowup", | |
| command: "setTranslateUp", | |
| args: 10, | |
| }, | |
| { | |
| desc: "Move down 10px", | |
| key: "shift+arrowdown", | |
| command: "setTranslateDown", | |
| args: -10, | |
| }, | |
| { | |
| desc: "Forward 5s", | |
| key: "arrowright", | |
| command: "setCurrentTimeUp", | |
| args: 5, | |
| }, | |
| { | |
| desc: "Back 5s", | |
| key: "arrowleft", | |
| command: "setCurrentTimeDown", | |
| args: -5, | |
| }, | |
| { | |
| desc: "Forward 30s", | |
| key: "ctrl+arrowright", | |
| command: "setCurrentTimeUp", | |
| args: [30], | |
| }, | |
| { | |
| desc: "Back 30s", | |
| key: "ctrl+arrowleft", | |
| command: "setCurrentTimeDown", | |
| args: [-30], | |
| }, | |
| { | |
| desc: "Volume +5%", | |
| key: "arrowup", | |
| command: "setVolumeUp", | |
| args: [0.05], | |
| }, | |
| { | |
| desc: "Volume -5%", | |
| key: "arrowdown", | |
| command: "setVolumeDown", | |
| args: [-0.05], | |
| }, | |
| { | |
| desc: "Volume +20%", | |
| key: "ctrl+arrowup", | |
| command: "setVolumeUp", | |
| args: [0.2], | |
| }, | |
| { | |
| desc: "Volume -20%", | |
| key: "ctrl+arrowdown", | |
| command: "setVolumeDown", | |
| args: [-0.2], | |
| }, | |
| { desc: "Toggle play/pause", key: "space", command: "switchPlayStatus" }, | |
| { | |
| desc: "Speed down -0.1", | |
| key: "x", | |
| command: "setPlaybackRateDown", | |
| args: -0.1, | |
| }, | |
| { | |
| desc: "Speed up +0.1", | |
| key: "c", | |
| command: "setPlaybackRateUp", | |
| args: 0.1, | |
| }, | |
| { desc: "Normal speed", key: "z", command: "resetPlaybackRate" }, | |
| { | |
| desc: "1x speed", | |
| key: "Digit1", | |
| command: "setPlaybackRatePlus", | |
| args: 1, | |
| }, | |
| { | |
| desc: "1x speed", | |
| key: "Numpad1", | |
| command: "setPlaybackRatePlus", | |
| args: 1, | |
| }, | |
| { | |
| desc: "2x speed", | |
| key: "Digit2", | |
| command: "setPlaybackRatePlus", | |
| args: 2, | |
| }, | |
| { | |
| desc: "2x speed", | |
| key: "Numpad2", | |
| command: "setPlaybackRatePlus", | |
| args: 2, | |
| }, | |
| { | |
| desc: "3x speed", | |
| key: "Digit3", | |
| command: "setPlaybackRatePlus", | |
| args: 3, | |
| }, | |
| { | |
| desc: "3x speed", | |
| key: "Numpad3", | |
| command: "setPlaybackRatePlus", | |
| args: 3, | |
| }, | |
| { | |
| desc: "4x speed", | |
| key: "Digit4", | |
| command: "setPlaybackRatePlus", | |
| args: 4, | |
| }, | |
| { | |
| desc: "4x speed", | |
| key: "Numpad4", | |
| command: "setPlaybackRatePlus", | |
| args: 4, | |
| }, | |
| { desc: "Next frame", key: "F", command: "freezeFrame", args: 1 }, | |
| { desc: "Previous frame", key: "D", command: "freezeFrame", args: -1 }, | |
| { desc: "Brightness up", key: "E", command: "setBrightnessUp" }, | |
| { desc: "Brightness down", key: "W", command: "setBrightnessDown" }, | |
| { desc: "Contrast up", key: "T", command: "setContrastUp" }, | |
| { desc: "Contrast down", key: "R", command: "setContrastDown" }, | |
| { desc: "Saturation up", key: "U", command: "setSaturationUp" }, | |
| { desc: "Saturation down", key: "Y", command: "setSaturationDown" }, | |
| { desc: "Hue up", key: "O", command: "setHueUp" }, | |
| { desc: "Hue down", key: "I", command: "setHueDown" }, | |
| { desc: "Blur up", key: "K", command: "setBlurUp" }, | |
| { desc: "Blur down", key: "J", command: "setBlurDown" }, | |
| { desc: "Reset filters", key: "Q", command: "resetFilterAndTransform" }, | |
| { desc: "Rotate 90°", key: "S", command: "setRotate" }, | |
| { desc: "Next video", key: "N", command: "setNextVideo" }, | |
| { desc: "Debugger", key: "ctrl+shift+alt+d", command: "debuggerNow" }, | |
| ]; | |
| const hasUseKey = { | |
| keyCodeList: [ | |
| 13, 16, 17, 18, 27, 32, 37, 38, 39, 40, 49, 50, 51, 52, 67, 68, 69, 70, | |
| 73, 74, 75, 77, 78, 79, 80, 81, 82, 83, 84, 85, 87, 88, 89, 90, 97, 98, | |
| 99, 100, 220, | |
| ], | |
| keyList: [ | |
| "enter", | |
| "shift", | |
| "control", | |
| "alt", | |
| "escape", | |
| " ", | |
| "arrowleft", | |
| "arrowright", | |
| "arrowup", | |
| "arrowdown", | |
| "1", | |
| "2", | |
| "3", | |
| "4", | |
| "c", | |
| "d", | |
| "e", | |
| "f", | |
| "i", | |
| "j", | |
| "k", | |
| "n", | |
| "o", | |
| "p", | |
| "q", | |
| "r", | |
| "s", | |
| "t", | |
| "u", | |
| "w", | |
| "x", | |
| "y", | |
| "z", | |
| "\\", | |
| "|", | |
| ], | |
| keyMap: { | |
| enter: 13, | |
| shift: 16, | |
| ctrl: 17, | |
| alt: 18, | |
| esc: 27, | |
| space: 32, | |
| "←": 37, | |
| "↑": 38, | |
| "→": 39, | |
| "↓": 40, | |
| 1: 49, | |
| 2: 50, | |
| 3: 51, | |
| 4: 52, | |
| }, | |
| }; | |
| /* ============================================================================ | |
| * SECTION 8: H5Player Core (Stub) | |
| * Main player logic - placeholder for full implementation | |
| * ============================================================================ */ | |
| const h5Player = { | |
| version: "4.4.0", | |
| mediaElementList: [], | |
| init() { | |
| console.log("[H5Player] Initializing v" + this.version); | |
| // Hack shadow DOM first | |
| hackAttachShadow(); | |
| // Initialize media core | |
| mediaCore.init((mediaElement) => { | |
| this.mediaElementList.push(mediaElement); | |
| console.log("[H5Player] Media element detected:", mediaElement.tagName); | |
| }); | |
| // Watch for new shadow roots | |
| document.addEventListener("addShadowRoot", (e) => { | |
| const shadowRoot = e.detail?.shadowRoot; | |
| if (shadowRoot) { | |
| ready( | |
| "video, audio", | |
| (el) => { | |
| console.log("[H5Player] Media in shadow DOM:", el.tagName); | |
| }, | |
| shadowRoot, | |
| ); | |
| } | |
| }); | |
| // Watch for media elements in main document | |
| ready("video, audio", (el) => { | |
| console.log("[H5Player] Ready:", el.tagName, el.src || "[no src]"); | |
| }); | |
| }, | |
| }; | |
| /* ============================================================================ | |
| * SECTION 9: Initialization | |
| * Script entry point | |
| * ============================================================================ */ | |
| // Initialize when DOM is ready | |
| if (document.readyState === "loading") { | |
| document.addEventListener("DOMContentLoaded", () => h5Player.init()); | |
| } else { | |
| h5Player.init(); | |
| } | |
| // Export for debugging | |
| window.h5Player = h5Player; | |
| window.mediaCore = mediaCore; | |
| console.log("[H5Player] Script loaded"); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment