Skip to content

Instantly share code, notes, and snippets.

@eplord
Last active February 10, 2026 20:22
Show Gist options
  • Select an option

  • Save eplord/964175a6f8f3a2c3d0b3fa9eb10c7262 to your computer and use it in GitHub Desktop.

Select an option

Save eplord/964175a6f8f3a2c3d0b3fa9eb10c7262 to your computer and use it in GitHub Desktop.
Mega Infinite - Audio/Video Enhancement Script
// ==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