// ==UserScript== // @name HTML5 Video Player Enhance // @version 2.9.5.2 // @description To enhance the functionality of HTML5 Video Player (h5player) supporting all websites using shortcut keys similar to PotPlayer. // @author CY Fung // @icon https://image.flaticon.com/icons/png/128/3291/3291444.png // @match http://*/* // @match https://*/* // @run-at document-start // @require https://cdnjs.cloudflare.com/ajax/libs/js-sha256/0.9.0/sha256.min.js // @namespace https://greasyfork.org/users/371179 // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant unsafeWindow // @downloadURL none // ==/UserScript== /** * Remarks * This script support modern browser only with ES6+. * fullscreen and pointerLock buggy in shadowRoot * Space Pause not success * shift F key issue **/ !(function($winUnsafe, $winSafe) { 'use strict'; !(() => 0)({ requestAnimationFrame, cancelAnimationFrame, MutationObserver, setInterval, clearInterval, EventTarget, Promise, ResizeObserver }); //throw Error if your browser is too outdated. (eg ES6 script, no such window object) const window = $winUnsafe || $winSafe const document = window.document const $$uWin = $winUnsafe || $winSafe; const $rAf = $$uWin.requestAnimationFrame; const $cAf = $$uWin.cancelAnimationFrame; const $$setTimeout = $$uWin.setTimeout const $$clearTimeout = $$uWin.clearTimeout const $$requestAnimationFrame = $$uWin.requestAnimationFrame; const $$cancelAnimationFrame = $$uWin.cancelAnimationFrame; const $$addEventListener = Node.prototype.addEventListener; const $$removeEventListener = Node.prototype.removeEventListener; const $bz = { boosted: false } const utPositioner='KVZX'; !(function $$() { 'use strict'; if (!document || !document.documentElement) return window.requestAnimationFrame($$); const prettyElm = function(elm) { if (!elm || !elm.nodeName) return null; const eId = elm.id || null; const eClsName = elm.className || null; return [elm.nodeName.toLowerCase(), typeof eId == 'string' ? "#" + eId : '', typeof eClsName == 'string' ? '.' + eClsName.replace(/\s+/g, '.') : ''].join('').trim(); } const delayCall = function(p, f, d) { if (delayCall[p] > 0) delayCall[p] = window.clearTimeout(delayCall[p]) if (f) delayCall[p] = window.setTimeout(f, d) } HTMLVideoElement.prototype.__isPlaying = function() { const video = this; return video.currentTime > 0 && !video.paused && !video.ended && video.readyState > video.HAVE_CURRENT_DATA; } const wmListeners = new WeakMap(); class Listeners { constructor() {} get count() { return (this._count || 0) } makeId() { return ++this._lastId } add(lh) { this[++this._lastId] = lh; } remove(lh_removal) { for (let k in this) { let lh = this[k] if (lh && lh.constructor == ListenerHandle && lh_removal.isEqual(lh)) { delete this[k]; this._count--; } } } } class ListenerHandle { constructor(func, options) { this.func = func this.options = options } isEqual(anotherLH) { if (this.func != anotherLH.func) return false; if (this.options === anotherLH.options) return true; if (this.options && anotherLH.options && typeof this.options == 'object' && typeof anotherLH.options == 'object') {} else { return false; } return this.uOpt() == anotherLH.uOpt() } uOpt() { let opt1 = ""; for (var k in this.options) { opt1 += ", " + k + " : " + (typeof this[k] == 'boolean' ? this[k] : "N/A"); } return opt1; } } Object.defineProperties(Listeners.prototype, { _lastId: { value: 0, writable: true, enumerable: false, configurable: true }, _count: { value: 0, writable: true, enumerable: false, configurable: true } }); let _debug_h5p_logging_ = false; try { _debug_h5p_logging_ = +window.localStorage.getItem('_h5_player_sLogging_') > 0 } catch (e) {} const SHIFT = 1; const CTRL = 2; const ALT = 4; const TERMINATE = 0x842; const _sVersion_ = 1817; const str_postMsgData = '__postMsgData__' const DOM_ACTIVE_FOUND = 1; const DOM_ACTIVE_SRC_LOADED = 2; const DOM_ACTIVE_ONCE_PLAYED = 4; const DOM_ACTIVE_MOUSE_CLICK = 8; const DOM_ACTIVE_MOUSE_IN = 16; const DOM_ACTIVE_DELAYED_PAUSED = 32; const DOM_ACTIVE_INVALID_PARENT = 2048; var console = {}; console.log = function() { window.console.log(...['[h5p]', ...arguments]) } console.error = function() { window.console.error(...['[h5p]', ...arguments]) } function makeNoRoot(shadowRoot) { const doc = shadowRoot.ownerDocument || document; const htmlInShadowRoot = doc.createElement('noroot'); // pseudo element const childNodes = [...shadowRoot.childNodes] shadowRoot.insertBefore(htmlInShadowRoot, shadowRoot.firstChild) for (const childNode of childNodes) htmlInShadowRoot.appendChild(childNode); return shadowRoot.querySelector('noroot'); } let _endlessloop = null; const isIframe = (window.top !== window.self && window.top && window.self); const shadowRoots = []; const _getRoot = Element.prototype.getRootNode || HTMLElement.prototype.getRootNode || function() { let elm = this; while (elm) { if ('host' in elm) return elm; elm = elm.parentNode; } return elm; } const getRoot = (elm) => _getRoot.call(elm); const isShadowRoot = (elm) => (elm && ('host' in elm)) ? elm.nodeType == 11 && !!elm.host && elm.host.nodeType == 1 : null; //instanceof ShadowRoot const domAppender = (d) => d.querySelector('head') || d.querySelector('html') || d.querySelector('noroot') || null; const playerConfs = {} const hanlderResizeVideo = (entries) => { const detected_changes = {}; for (let entry of entries) { const player = entry.target.nodeName == "VIDEO" ? entry.target : entry.target.querySelector("VIDEO[_h5ppid]"); if (!player) continue; const vpid = player.getAttribute('_h5ppid'); if (!vpid) continue; if (vpid in detected_changes) continue; detected_changes[vpid] = true; const {wPlayerInner,wPlayer} = $hs.getPlayerBlockElement(player) if (!wPlayerInner) continue; const layoutBoxInner = wPlayerInner.parentNode if (!layoutBoxInner) continue; let tipsDom = layoutBoxInner.querySelector('[data-h5p-pot-tips]'); if (tipsDom) { if (tipsDom._tips_display_none) tipsDom.setAttribute('data-h5p-pot-tips', '') $hs.fixNonBoxingVideoTipsPosition(tipsDom, player); } else { tipsDom = getRoot(player).querySelector(`#${player.getAttribute('_h5player_tips')}`) if (tipsDom) { if (tipsDom._tips_display_none) tipsDom.setAttribute('data-h5p-pot-tips', '') $hs.change_layoutBox(tipsDom, player); $hs.tipsDomObserve(tipsDom, player); } } } }; const $mb = { nightly_isSupportQueueMicrotask: function() { if ('_isSupportQueueMicrotask' in $mb) return $mb._isSupportQueueMicrotask; $mb._isSupportQueueMicrotask = false; $mb.queueMicrotask = window.queueMicrotask; if (typeof $mb.queueMicrotask == 'function') { $mb._isSupportQueueMicrotask = true; } return $mb._isSupportQueueMicrotask; }, stable_isSupportAdvancedEventListener: function() { if ('_isSupportAdvancedEventListener' in $mb) return $mb._isSupportAdvancedEventListener let prop = 0; $$addEventListener.call(document.createAttribute('z'), 'z', () => 0, { get passive() { prop++; }, get once() { prop++; } }); return ($mb._isSupportAdvancedEventListener = (prop == 2)); }, stable_isSupportPassiveEventListener: function() { if ('_isSupportPassiveEventListener' in $mb) return $mb._isSupportPassiveEventListener let prop = 0; $$addEventListener.call(document.createAttribute('z'), 'z', () => 0, { get passive() { prop++; } }); return ($mb._isSupportPassiveEventListener = (prop == 1)); }, eh_capture_passive: () => ($mb._eh_capture_passive = $mb._eh_capture_passive || ($mb.stable_isSupportPassiveEventListener() ? { capture: true, passive: true } : true)), eh_bubble_passive: () => ($mb._eh_capture_passive = $mb._eh_capture_passive || ($mb.stable_isSupportPassiveEventListener() ? { capture: false, passive: true } : false)) } Element.prototype.__matches__ = (Element.prototype.matches || Element.prototype.matchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.webkitMatchesSelector || Element.prototype.matches()); // throw Error if not supported // built-in hash - https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest async function digestMessage(message) { return $winSafe.sha256(message) } const dround = (x) => ~~(x + .5); const jsonStringify_replacer = function(key, val) { if (val && (val instanceof Element || val instanceof Document)) return val.toString(); return val; // return as is }; const jsonParse = function() { try { return JSON.parse.apply(this, arguments) } catch (e) {} return null; } const jsonStringify = function(obj) { try { return JSON.stringify.call(this, obj, jsonStringify_replacer) } catch (e) {} return null; } function _postMsg() { //async is needed. or error handling for postMessage const [win, tag, ...data] = arguments; if (typeof tag == 'string') { let postMsgObj = { tag, passing: true, winOrder: _postMsg.a } try { let k = 'msg-' + (+new Date) win.document[str_postMsgData] = win.document[str_postMsgData] || {} win.document[str_postMsgData][k] = data; //direct postMsgObj.str = k; postMsgObj.stype = 1; } catch (e) {} if (!postMsgObj.stype) { postMsgObj.str = jsonStringify({ d: data }) if (postMsgObj.str && postMsgObj.str.length) postMsgObj.stype = 2; } if (!postMsgObj.stype) { postMsgObj.str = "" + data; postMsgObj.stype = 0; } win.postMessage(postMsgObj, '*'); } } function postMsg() { let win = window; let a = 0; while ((win = win.parent) && ('postMessage' in win)) { _postMsg.a = ++a; _postMsg(win, ...arguments) if (win == top) break; } } function crossBrowserTransition(type) { if (crossBrowserTransition['_result_' + type]) return crossBrowserTransition['_result_' + type] let el = document.createElement("fakeelement"); const capital = (x) => x[0].toUpperCase() + x.substr(1); const capitalType = capital(type); const transitions = { [type]: `${type}end`, [`O${capitalType}`]: `o${capitalType}End`, [`Moz${capitalType}`]: `${type}end`, [`Webkit${capitalType}`]: `webkit${capitalType}End`, [`MS${capitalType}`]: `MS${capitalType}End` } for (let styleProp in transitions) { if (el.style[styleProp] !== undefined) { return (crossBrowserTransition['_result_' + type] = transitions[styleProp]); } } } function isInOperation(elm) { let elmInFocus = elm || document.activeElement; if (!elmInFocus) return false; let res1 = elmInFocus.__matches__( 'a[href],link[href],button,input:not([type="hidden"]),select,textarea,iframe,frame,menuitem,[draggable],[contenteditable]' ); return res1; } const fn_toString = (f, n = 50) => { let s = (f + ""); if (s.length > 2 * n + 5) { s = s.substr(0, n) + ' ... ' + s.substr(-n); } return s }; function consoleLog() { if (!_debug_h5p_logging_) return; if (isIframe) postMsg('consoleLog', ...arguments); else console.log.apply(console, arguments); } function consoleLogF() { if (isIframe) postMsg('consoleLog', ...arguments); else console.log.apply(console, arguments); } class AFLooperArray extends Array { constructor() { super(); this.activeLoopsCount = 0; this.cid = 0; this.loopingFrame = this.loopingFrame.bind(this); } loopingFrame() { if (!this.cid) return; //cancelled for (const opt of this) { if (opt.isFunctionLooping) opt.fn(); } } get isArrayLooping() { return this.cid > 0; } loopStart() { this.cid = window.setInterval(this.loopingFrame, 300); } loopStop() { if (this.cid) window.clearInterval(this.cid); this.cid = 0; } appendLoop(fn) { if (typeof fn != 'function' || !this) return; const opt = new AFLooperFunc(fn, this); super.push(opt); return opt; } } class AFLooperFunc { constructor(fn, bind) { this._looping = false; this.bind = bind; this.fn = fn; } get isFunctionLooping() { return this._looping; } loopingStart() { if (this._looping === false) { this._looping = true; if (++this.bind.activeLoopsCount == 1) this.bind.loopStart(); } } loopingStop() { if (this._looping === true) { this._looping = false; if (--this.bind.activeLoopsCount == 0) this.bind.loopStop(); } } } function decimalEqual(a, b) { return Math.round(a * 100000000) == Math.round(b * 100000000) } function nonZeroNum(a) { return a > 0 || a < 0; } class PlayerConf { get scaleFactor() { return this.mFactor * this.vFactor; } cssTransform() { const playerConf = this; const player = playerConf.domElement; if (!player) return; const videoScale = playerConf.scaleFactor; let { x, y } = playerConf.translate; let [_x, _y] = ((playerConf.rotate % 180) == 90) ? [y, x] : [x, y]; if ((playerConf.rotate % 360) == 270) _x = -_x; if ((playerConf.rotate % 360) == 90) _y = -_y; var s = [ playerConf.rotate > 0 ? 'rotate(' + playerConf.rotate + 'deg)' : '', !decimalEqual(videoScale, 1.0) ? 'scale(' + videoScale + ')' : '', (nonZeroNum(_x) || nonZeroNum(_y)) ? `translate(${_x}px, ${_y}px)` : '', ]; player.style.transform = s.join(' ').trim() } constructor() { this.translate = { x: 0, y: 0 }; this.rotate = 0; this.mFactor = 1.0; this.vFactor = 1.0; this.fps = 30; this.filter_key = {}; this.filter_view_units = { 'hue-rotate': 'deg', 'blur': 'px' }; this.filterReset(); } setFilter(prop, f) { let oldValue = this.filter_key[prop]; if (typeof oldValue != 'number') return; let newValue = f(oldValue) if (oldValue != newValue) { newValue = +newValue.toFixed(6); //javascript bug } this.filter_key[prop] = newValue this.filterSetup(); return newValue; } filterSetup(options) { let ums = GM_getValue("unsharpen_mask") if (!ums) ums = "" let view = [] let playerElm = $hs.player(); if (!playerElm) return; for (let view_key in this.filter_key) { let filter_value = +((+this.filter_key[view_key] || 0).toFixed(3)) let addTo = true; switch (view_key) { case 'brightness': /* fall through */ case 'contrast': /* fall through */ case 'saturate': if (decimalEqual(filter_value, 1.0)) addTo = false; break; case 'hue-rotate': /* fall through */ case 'blur': if (decimalEqual(filter_value, 0.0)) addTo = false; break; } let view_unit = this.filter_view_units[view_key] || '' if (addTo) view.push(`${view_key}(${filter_value}${view_unit})`) this.filter_key[view_key] = Number(+this.filter_key[view_key] || 0) } if (ums) view.push(`url("#_h5p_${ums}")`); if (options && options.grey) view.push('url("#grey1")'); playerElm.style.filter = view.join(' ').trim(); //performance in firefox is bad } filterReset() { this.filter_key['brightness'] = 1.0 this.filter_key['contrast'] = 1.0 this.filter_key['saturate'] = 1.0 this.filter_key['hue-rotate'] = 0.0 this.filter_key['blur'] = 0.0 this.filterSetup() } } const Store = { prefix: '_h5_player', save: function(k, v) { if (!Store.available()) return false; if (typeof v != 'string') return false; Store.LS.setItem(Store.prefix + k, v) let sk = fn_toString(k + "", 30); let sv = fn_toString(v + "", 30); consoleLog(`localStorage Saved "${sk}" = "${sv}"`) return true; }, read: function(k) { if (!Store.available()) return false; let v = Store.LS.getItem(Store.prefix + k) let sk = fn_toString(k + "", 30); let sv = fn_toString(v + "", 30); consoleLog(`localStorage Read "${sk}" = "${sv}"`); return v; }, remove: function(k) { if (!Store.available()) return false; Store.LS.removeItem(Store.prefix + k) let sk = fn_toString(k + "", 30); consoleLog(`localStorage Removed "${sk}"`) return true; }, clearInvalid: function(sVersion) { if (!Store.available()) return false; //let sVersion=1814; if (+Store.read('_sVersion_') < sVersion) { Store._keys() .filter(s => s.indexOf(Store.prefix) === 0) .forEach(key => window.localStorage.removeItem(key)) Store.save('_sVersion_', sVersion + '') return 2; } return 1; }, available: function() { if (Store.LS) return true; if (!window) return false; const localStorage = window.localStorage; if (!localStorage) return false; if (typeof localStorage != 'object') return false; if (!('getItem' in localStorage)) return false; if (!('setItem' in localStorage)) return false; Store.LS = localStorage; return true; }, _keys: function() { return Object.keys(localStorage); }, _setItem: function(key, value) { return localStorage.setItem(key, value) }, _getItem: function(key) { return localStorage.getItem(key) }, _removeItem: function(key) { return localStorage.removeItem(key) } } const domTool = { nopx: (x) => +x.replace('px', ''), cssWH: function(m, r) { if (!r) r = getComputedStyle(m, null); let c = (x) => +x.replace('px', ''); return { w: m.offsetWidth || c(r.width), h: m.offsetHeight || c(r.height) } }, _isActionBox_1: function(vEl, pEl) { const vElCSS = domTool.cssWH(vEl); let vElCSSw = vElCSS.w; let vElCSSh = vElCSS.h; let vElx = vEl; const res = []; //let mLevel = 0; if (vEl && pEl && vEl != pEl && pEl.contains(vEl)) { while (vElx && vElx != pEl) { vElx = vElx.parentNode; let vElx_css = null; if (isShadowRoot(vElx)) {} else { vElx_css = getComputedStyle(vElx, null); let vElx_wp = domTool.nopx(vElx_css.paddingLeft) + domTool.nopx(vElx_css.paddingRight) vElCSSw += vElx_wp let vElx_hp = domTool.nopx(vElx_css.paddingTop) + domTool.nopx(vElx_css.paddingBottom) vElCSSh += vElx_hp } res.push({ //level: ++mLevel, padW: vElCSSw, padH: vElCSSh, elm: vElx, css: vElx_css }) } } // in the array, each item is the parent of video player //res.vEl_cssWH = vElCSS return res; }, _isActionBox: function(vEl, walkRes, pEl_idx) { function absDiff(w1, w2, h1, h2) { const w = (w1 - w2), h = h1 - h2; return [(w > 0 ? w : -w), (h > 0 ? h : -h)] } function midPoint(rect) { return { x: (rect.left + rect.right) / 2, y: (rect.top + rect.bottom) / 2 } } const parentCount = walkRes.length; if (pEl_idx >= 0 && pEl_idx < parentCount) {} else { return; } const pElr = walkRes[pEl_idx] if (!pElr.css) { //shadowRoot return true; } const pEl = pElr.elm; //prevent activeElement==body const pElCSS = domTool.cssWH(pEl, pElr.css); //check prediction of parent dimension const d1v = absDiff(pElCSS.w, pElr.padW, pElCSS.h, pElr.padH) const d1x = d1v[0] < 10 const d1y = d1v[1] < 10; if (d1x && d1y) return true; //both edge along the container - fit size if (!d1x && !d1y) return false; //no edge along the container - body contain the video element, fixed width&height //case: youtube video fullscreen //check centre point const pEl_rect = pEl.getBoundingClientRect() const vEl_rect = vEl.getBoundingClientRect() const pEl_center = midPoint(pEl_rect) const vEl_center = midPoint(vEl_rect) const d2v = absDiff(pEl_center.x, vEl_center.x, pEl_center.y, vEl_center.y); const d2x = d2v[0] < 10; const d2y = d2v[1] < 10; return (d2x && d2y); }, getRect: function(element) { let rect = element.getBoundingClientRect(); let scroll = domTool.getScroll(); return { pageX: rect.left + scroll.left, pageY: rect.top + scroll.top, screenX: rect.left, screenY: rect.top }; }, getScroll: function() { return { left: document.documentElement.scrollLeft || document.body.scrollLeft, top: document.documentElement.scrollTop || document.body.scrollTop }; }, getClient: function() { return { width: document.compatMode == 'CSS1Compat' ? document.documentElement.clientWidth : document.body.clientWidth, height: document.compatMode == 'CSS1Compat' ? document.documentElement.clientHeight : document.body.clientHeight }; }, addStyle: //GM_addStyle, function(css, head) { if (!head) { let _doc = document.documentElement; head = domAppender(_doc); } let doc = head.ownerDocument; let style = doc.createElement('style'); style.type = 'text/css'; style.textContent = css; head.appendChild(style); //console.log(document.head,style,'add style') return style; }, eachParentNode: function(dom, fn) { let parent = dom.parentNode while (parent) { let isEnd = fn(parent, dom) parent = parent.parentNode if (isEnd) { break } } }, hideDom: function hideDom(selector) { let dom = document.querySelector(selector) if (dom) { window.requestAnimationFrame(function() { dom.style.opacity = 0; dom.style.transform = 'translate(-9999px)'; dom = null; }) } } }; const handle = { afPlaybackRecording: async function() { const opts = this; let qTime = +new Date; if (qTime >= opts.pTime) { opts.pTime = qTime + opts.timeDelta; //prediction of next Interval opts.savePlaybackProgress() } }, savePlaybackProgress: function() { //this refer to endless's opts let player = this.player; let _uid = this.player_uid; //_h5p_uid_encrypted if (!_uid) return; let shallSave = true; let currentTimeToSave = ~~player.currentTime; if (this._lastSave == currentTimeToSave) shallSave = false; if (shallSave) { this._lastSave = currentTimeToSave Promise.resolve().then(() => { //console.log('aasas',this.player_uid, shallSave, '_play_progress_'+_uid, currentTimeToSave) Store.save('_play_progress_' + _uid, jsonStringify({ 't': currentTimeToSave })) }) } //console.log('playback logged') }, playingWithRecording: function() { let player = this.player; if (!player.paused && !this.isFunctionLooping) { let player = this.player; let _uid = player.getAttribute('_h5p_uid_encrypted') || '' if (_uid) { this.player_uid = _uid; this.pTime = 0; this.loopingStart(); } } } }; /* class Momentary extends Map { act(uniqueId, fn_start, fn_end, delay) { if (!uniqueId) return; uniqueId = uniqueId + ""; const last_cid = this.get(uniqueId); if (last_cid > 0) window.clearTimeout(last_cid); fn_start(); const new_cid = window.setTimeout(fn_end, delay) this.set(uniqueId, new_cid) } } const momentary = new Momentary();*/ const $hs = { /* 提示文本的字號 */ fontSize: 16, enable: true, playerInstance: null, playbackRate: 1, /* 快進快退步長 */ skipStep: 5, /* 獲取當前播放器的實例 */ player: function() { let res = $hs.playerInstance || null; if (res && res.parentNode == null) { $hs.playerInstance = null; res = null; } if (res == null) { for (let k in playerConfs) { let playerConf = playerConfs[k]; if (playerConf && playerConf.domElement && playerConf.domElement.parentNode) return playerConf.domElement; } } return res; }, pictureInPicture: function(videoElm) { if (document.pictureInPictureElement) { document.exitPictureInPicture(); } else if ('requestPictureInPicture' in videoElm) { videoElm.requestPictureInPicture() } else { $hs.tips('PIP is not supported.'); } }, getPlayerConf: function(video) { if (!video) return null; let vpid = video.getAttribute('_h5ppid') || null; if (!vpid) return null; return playerConfs[vpid] || null; }, debug01: function(evt, videoActive) { if (!$hs.eventHooks) { document.__h5p_eventhooks = ($hs.eventHooks = { _debug_: [] }); } $hs.eventHooks._debug_.push([videoActive, evt.type]); // console.log('h5p eventhooks = document.__h5p_eventhooks') }, swtichPlayerInstance: function() { let newPlayerInstance = null; const ONLY_PLAYING_NONE = 0x4A00; const ONLY_PLAYING_MORE_THAN_ONE = 0x5A00; let onlyPlayingInstance = ONLY_PLAYING_NONE; for (let k in playerConfs) { let playerConf = playerConfs[k] || {}; let { domElement, domActive } = playerConf; if (domElement) { if (domActive & DOM_ACTIVE_INVALID_PARENT) continue; if (!domElement.parentNode) { playerConf.domActive |= DOM_ACTIVE_INVALID_PARENT; continue; } if (domActive & DOM_ACTIVE_MOUSE_CLICK) { newPlayerInstance = domElement break; } if (domActive & DOM_ACTIVE_ONCE_PLAYED && (domActive & DOM_ACTIVE_DELAYED_PAUSED) == 0) { if (onlyPlayingInstance == ONLY_PLAYING_NONE) onlyPlayingInstance = domElement; else onlyPlayingInstance = ONLY_PLAYING_MORE_THAN_ONE; } } } if (newPlayerInstance == null && onlyPlayingInstance.nodeType == 1) { newPlayerInstance = onlyPlayingInstance; } $hs.playerInstance = newPlayerInstance }, mouseMoveCount: 0, handlerVideoPlaying: function(evt) { const videoElm = evt.target || this || null; if (!videoElm || videoElm.nodeName != "VIDEO") return; const vpid = videoElm.getAttribute('_h5ppid') if (!vpid) return; Promise.resolve().then(() => { if ($hs.cid_playHook > 0) window.clearTimeout($hs.cid_playHook); $hs.cid_playHook = window.setTimeout(function() { let onlyPlayed = null; for (var k in playerConfs) { if (k == vpid) { if (playerConfs[k].domElement.paused === false) onlyPlayed = true; } else if (playerConfs[k].domElement.paused === false) { onlyPlayed = false; break; } } if (onlyPlayed === true) { $hs.focusHookVDoc = getRoot(videoElm) $hs.focusHookVId = vpid } $bv.boostVideoPerformanceActivate(); $hs.hcDelayMouseHideAndStartMointoring(videoElm); }, 100) }).then(() => { const playerConf = $hs.getPlayerConf(videoElm) if (playerConf) { if (playerConf.timeout_pause > 0) playerConf.timeout_pause = window.clearTimeout(playerConf.timeout_pause); playerConf.lastPauseAt = 0 playerConf.domActive |= DOM_ACTIVE_ONCE_PLAYED; playerConf.domActive &= ~DOM_ACTIVE_DELAYED_PAUSED; } }).then(() => { $hs._actionBoxObtain(videoElm); }).then(() => { $hs.swtichPlayerInstance(); $hs.onVideoTriggering(); }).then(() => { if (!$hs.enable) return $hs.tips(false); if (videoElm._isThisPausedBefore_) consoleLog('resumed') let _pausedbefore_ = videoElm._isThisPausedBefore_ if (videoElm.playpause_cid) { window.clearTimeout(videoElm.playpause_cid); videoElm.playpause_cid = 0; } let _last_paused = videoElm._last_paused videoElm._last_paused = videoElm.paused if (_last_paused === !videoElm.paused) { videoElm.playpause_cid = window.setTimeout(() => { if (videoElm.paused === !_last_paused && !videoElm.paused && _pausedbefore_) { $hs.tips('Playback resumed', undefined, 2500) } }, 90) } /* 播放的時候進行相關同步操作 */ if (!videoElm._record_continuous) { /* 同步之前設定的播放速度 */ $hs.setPlaybackRate() if (!_endlessloop) _endlessloop = new AFLooperArray(); videoElm._record_continuous = _endlessloop.appendLoop(handle.afPlaybackRecording); videoElm._record_continuous._lastSave = -999; videoElm._record_continuous.timeDelta = 2000; videoElm._record_continuous.player = videoElm videoElm._record_continuous.savePlaybackProgress = handle.savePlaybackProgress; videoElm._record_continuous.playingWithRecording = handle.playingWithRecording; } videoElm._record_continuous.playingWithRecording(videoElm); //try to start recording videoElm._isThisPausedBefore_ = false; }) }, handlerVideoPause: function(evt) { const videoElm = evt.target || this || null; if (!videoElm || videoElm.nodeName != "VIDEO") return; const vpid = videoElm.getAttribute('_h5ppid') if (!vpid) return; Promise.resolve().then(() => { if ($hs.cid_playHook > 0) window.clearTimeout($hs.cid_playHook); $hs.cid_playHook = window.setTimeout(function() { let allPaused = true; for (var k in playerConfs) { if (playerConfs[k].domElement.paused === false) { allPaused = false; break; } } if (allPaused) { $hs.focusHookVDoc = getRoot(videoElm) $hs.focusHookVId = vpid } $bv.boostVideoPerformanceDeactivate(); }, 100) }).then(() => { const playerConf = $hs.getPlayerConf(videoElm) if (playerConf) { playerConf.lastPauseAt = +new Date; playerConf.timeout_pause = window.setTimeout(() => { if (playerConf.lastPauseAt > 0) playerConf.domActive |= DOM_ACTIVE_DELAYED_PAUSED; }, 600) } }).then(() => { if (!$hs.enable) return $hs.tips(false); consoleLog('pause') videoElm._isThisPausedBefore_ = true; let _last_paused = videoElm._last_paused videoElm._last_paused = videoElm.paused if (videoElm.playpause_cid) { window.clearTimeout(videoElm.playpause_cid); videoElm.playpause_cid = 0; } if (_last_paused === !videoElm.paused) { videoElm.playpause_cid = window.setTimeout(() => { if (videoElm.paused === !_last_paused && videoElm.paused) { $hs._tips(videoElm, 'Playback paused', undefined, 2500) } }, 90) } if (videoElm._record_continuous && videoElm._record_continuous.isFunctionLooping) { window.setTimeout(function() { if (videoElm.paused === true && !videoElm._record_continuous.isFunctionLooping) videoElm._record_continuous.savePlaybackProgress(); //savePlaybackProgress once before stopping //handle.savePlaybackProgress; }, 380) videoElm._record_continuous.loopingStop(); } }) }, handlerVideoVolumeChange: function(evt) { let videoElm = evt.target || this || null; if (videoElm.nodeName != "VIDEO") return; if (videoElm.volume >= 0) {} else { return; } if ($hs._volume_change_counter > 0) return; $hs._volume_change_counter = ($hs._volume_change_counter || 0) + 1 window.requestAnimationFrame(function() { let makeTips = false; Promise.resolve(videoElm).then((videoElm) => { let cVol = videoElm.volume; let cMuted = videoElm.muted; if (cVol === videoElm._volume_p && cMuted === videoElm._muted_p) { // nothing changed } else if (cVol === videoElm._volume_p && cMuted !== videoElm._muted_p) { // muted changed } else { // cVol != pVol // only volume changed let shallShowTips = videoElm._volume >= 0; //prevent initialization if (!cVol) { videoElm.muted = true; } else if (cMuted) { videoElm.muted = false; videoElm._volume = cVol; } else if (!cMuted) { videoElm._volume = cVol; } consoleLog('volume changed'); if (shallShowTips) makeTips = true; } videoElm._volume_p = cVol; videoElm._muted_p = cMuted; return videoElm; }).then((videoElm) => { if (makeTips) $hs._tips(videoElm, 'Volume: ' + dround(videoElm.volume * 100) + '%', undefined, 3000); $hs._volume_change_counter = 0; }) videoElm = null }) }, handlerVideoLoadedMetaData: function(evt) { const videoElm = evt.target || this || null; if (!videoElm || videoElm.nodeName != "VIDEO") return; Promise.resolve(videoElm).then((videoElm) => { consoleLog('video size', videoElm.videoWidth + ' x ' + videoElm.videoHeight); let vpid = videoElm.getAttribute('_h5ppid') || null; if (!vpid || !videoElm.currentSrc) return; let videoElm_withSrcChanged = null if ($hs.varSrcList[vpid] != videoElm.currentSrc) { $hs.varSrcList[vpid] = videoElm.currentSrc; $hs.videoSrcFound(videoElm); videoElm_withSrcChanged = videoElm; } if (!videoElm._onceVideoLoaded) { videoElm._onceVideoLoaded = true; playerConfs[vpid].domActive |= DOM_ACTIVE_SRC_LOADED; } return videoElm_withSrcChanged }).then((videoElm_withSrcChanged) => { if (videoElm_withSrcChanged) $hs._actionBoxObtain(videoElm_withSrcChanged); }) }, handlerSizing:(entries)=>{ for(const {target} of entries){ let cw=target.clientWidth let ch=target.clientHeight target.__clientWidth = cw target.__clientHeight = ch target.mouseMoveMax = Math.sqrt(cw * cw + ch * ch) * 0.06; } }, mouseActioner: { calls: [], time: 0, cid: 0, lastFound: null, lastHoverElm: null }, mouseEnteredElement: null, mouseAct: function() { $hs.mouseActioner.cid = 0; if (+new Date - $hs.mouseActioner.time < 30) { $hs.mouseActioner.cid = window.setTimeout($hs.mouseAct, 82) return; } if ($hs.mouseDownAt && $hs.mouseActioner.lastFound && $hs.mouseDownAt.insideVideo === $hs.mouseActioner.lastFound) { return; } const getVideo = (target) => { const actionBoxRelation = $hs.getActionBoxRelationFromDOM(target); if (!actionBoxRelation) return; const actionBox = actionBoxRelation.actionBox if (!actionBox) return; const vpid = actionBox.getAttribute('_h5p_actionbox_'); const videoElm = actionBoxRelation.player; if (!videoElm) return; return videoElm } Promise.resolve().then(() => { for (const { type, target } of $hs.mouseActioner.calls) { if (type == 'mouseenter') { const videoElm = getVideo(target); if (videoElm) { return videoElm } } } return null; }).then(videoFound => { Promise.resolve().then(() => { var plastHoverElm = $hs.mouseActioner.lastHoverElm; $hs.mouseActioner.lastHoverElm = $hs.mouseActioner.calls[0] ? $hs.mouseActioner.calls[0].target : null //console.log(!!$hs.mointoringVideo , !!videoFound) //console.log(554,'mointoringVideo:'+!!$hs.mointoringVideo,'videoFound:'+ !!videoFound) if ($hs.mointoringVideo && !videoFound) { $hs.hcShowMouseAndRemoveMointoring($hs.mointoringVideo) } else if ($hs.mointoringVideo && videoFound) { if (plastHoverElm != $hs.mouseActioner.lastHoverElm) $hs.hcMouseShowWithMonitoring(videoFound); } else if (!$hs.mointoringVideo && videoFound) { $hs.hcDelayMouseHideAndStartMointoring(videoFound) } $hs.mouseMoveCount = 0; $hs.mouseActioner.calls.length = 0; $hs.mouseActioner.lastFound = videoFound; }) if (videoFound !== $hs.mouseActioner.lastFound) { if ($hs.mouseActioner.lastFound) { $hs.handlerElementMouseLeaveVideo($hs.mouseActioner.lastFound) } if (videoFound) { $hs.handlerElementMouseEnterVideo(videoFound) } } }) }, handlerElementMouseEnterVideo: function(video) { //console.log('mouseenter video') const playerConf = $hs.getPlayerConf(video) if (playerConf) { playerConf.domActive |= DOM_ACTIVE_MOUSE_IN; } $hs._actionBoxObtain(video); $hs.enteredActionBoxRelation = $hs.actionBoxRelations[video.getAttribute('_h5ppid') || 'null'] || null }, handlerElementMouseLeaveVideo: function(video) { //console.log('mouseleave video') const playerConf = $hs.getPlayerConf(video) if (playerConf) { playerConf.domActive &= ~DOM_ACTIVE_MOUSE_IN; } $hs.enteredActionBoxRelation = null }, handlerElementMouseEnter: function(evt) { if ($hs.intVideoInitCount > 0) {} else { return; } if (!evt || !evt.target || !(evt.target.nodeType > 0)) return; $hs.mouseEnteredElement = evt.target if ($hs.mouseDownAt && $hs.mouseDownAt.insideVideo) return; if ($hs.enteredActionBoxRelation && $hs.enteredActionBoxRelation.pContainer && $hs.enteredActionBoxRelation.pContainer.contains(evt.target)) return; //console.log('mouseenter call') $hs.mouseActioner.calls.length = 1; $hs.mouseActioner.calls[0] = { type: evt.type, target: evt.target } //$hs.mouseActioner.calls.push({type:evt.type,target:evt.target}); $hs.mouseActioner.time = +new Date; if (!$hs.mouseActioner.cid) { $hs.mouseActioner.cid = window.setTimeout($hs.mouseAct, 82) } //console.log(evt.target) }, handlerElementMouseLeave: function(evt) { if ($hs.intVideoInitCount > 0) {} else { return; } if (!evt || !evt.target || !(evt.target.nodeType > 0)) return; if ($hs.mouseDownAt && $hs.mouseDownAt.insideVideo) return; if ($hs.enteredActionBoxRelation && $hs.enteredActionBoxRelation.pContainer && !$hs.enteredActionBoxRelation.pContainer.contains(evt.target)) { //console.log('mouseleave call') //$hs.mouseActioner.calls.push({type:evt.type,target:evt.target}); $hs.mouseActioner.time = +new Date; if (!$hs.mouseActioner.cid) { $hs.mouseActioner.cid = window.setTimeout($hs.mouseAct, 82) } } }, handlerElementMouseDown: function(evt) { if ($hs.mouseDownAt) return; $hs.mouseDownAt = { elm: evt.target, insideVideo: false, pContainer: null }; if ($hs.intVideoInitCount > 0) {} else { return; } // $hs._mouseIsDown=true; if (!evt || !evt.target || !(evt.target.nodeType > 0)) return; if ($hs.mouseActioner.lastFound && $hs.mointoringVideo) $hs.hcMouseShowWithMonitoring($hs.mouseActioner.lastFound) Promise.resolve(evt.target).then((evtTarget) => { if (document.readyState != "complete") return; function notAtVideo() { if ($hs.focusHookVDoc) $hs.focusHookVDoc = null if ($hs.focusHookVId) $hs.focusHookVId = '' } const actionBoxRelation = $hs.getActionBoxRelationFromDOM(evtTarget); if (!actionBoxRelation) return notAtVideo(); const actionBox = actionBoxRelation.actionBox if (!actionBox) return notAtVideo(); const vpid = actionBox.getAttribute('_h5p_actionbox_'); const videoElm = actionBoxRelation.player; if (!videoElm) return notAtVideo(); if (!$hs.mouseDownAt) return; $hs.mouseDownAt.insideVideo = videoElm; $hs.mouseDownAt.pContainer = actionBoxRelation.pContainer; if (vpid) { $hs.focusHookVDoc = getRoot(videoElm) $hs.focusHookVId = vpid } const playerConf = $hs.getPlayerConf(videoElm) if (playerConf) { delayCall("$$actionBoxClicking", function() { playerConf.domActive &= ~DOM_ACTIVE_MOUSE_CLICK; }, 300) playerConf.domActive |= DOM_ACTIVE_MOUSE_CLICK; } return videoElm }).then((videoElm) => { if (!videoElm) return; $hs._actionBoxObtain(videoElm); return videoElm }).then((videoElm) => { if (!videoElm) return; $hs.swtichPlayerInstance(); }) }, handlerElementMouseUp: function(evt) { if ($hs.pendingTips) { let pendingTips = $hs.pendingTips; $hs.pendingTips = null; for (let vpid in pendingTips) { const tipsDom = pendingTips[vpid] Promise.resolve(tipsDom).then(() => { if (tipsDom.getAttribute('_h5p_animate') == '0') tipsDom.setAttribute('_h5p_animate', '1'); }) } pendingTips = null; } if ($hs.mouseDownAt) { $hs.mouseDownAt = null; } }, handlerElementWheelTuneVolume: function(evt) { //shift + wheel if ($hs.intVideoInitCount > 0) {} else { return; } if (!evt.shiftKey || !evt.target || !(evt.target.nodeType > 0)) return; const fDeltaY = (evt.deltaY > 0) ? 1 : (evt.deltaY < 0) ? -1 : 0; if (fDeltaY) { const randomID = +new Date $hs.handlerElementWheelTuneVolume._randomID = randomID; Promise.resolve(evt.target).then((evtTarget) => { const actionBoxRelation = $hs.getActionBoxRelationFromDOM(evtTarget); if (!actionBoxRelation) return; const actionBox = actionBoxRelation.actionBox if (!actionBox) return; const vpid = actionBox.getAttribute('_h5p_actionbox_'); const videoElm = actionBoxRelation.player; if (!videoElm) return; let player = $hs.player(); if (!player || player != videoElm) return; return videoElm }).then((videoElm) => { if (!videoElm) return; if ($hs.handlerElementWheelTuneVolume._randomID != randomID) return; // $hs._actionBoxObtain(videoElm); return videoElm; }).then((player) => { if (!player) return; if ($hs.handlerElementWheelTuneVolume._randomID != randomID) return; if (fDeltaY > 0) { if ((player.muted && player.volume === 0) && player._volume > 0) { player.muted = false; player.volume = player._volume; } else if (player.muted && (player.volume > 0 || !player._volume)) { player.muted = false; } $hs.tuneVolume(-0.05) } else if (fDeltaY < 0) { if ((player.muted && player.volume === 0) && player._volume > 0) { player.muted = false; player.volume = player._volume; } else if (player.muted && (player.volume > 0 || !player._volume)) { player.muted = false; } $hs.tuneVolume(+0.05) } }) evt.stopPropagation() evt.preventDefault() return false } }, handlerWinMessage: async function(e) { let tag, ed; if (typeof e.data == 'object' && typeof e.data.tag == 'string') { tag = e.data.tag; ed = e.data } else { return; } let msg = null, success = 0; let msg_str, msg_stype, p switch (tag) { case 'consoleLog': msg_str = ed.str; msg_stype = ed.stype; if (msg_stype === 1) { msg = (document[str_postMsgData] || {})[msg_str] || []; success = 1; } else if (msg_stype === 2) { msg = jsonParse(msg_str); if (msg && msg.d) { success = 2; msg = msg.d; } } else { msg = msg_str } p = (ed.passing && ed.winOrder) ? [' | from win-' + ed.winOrder] : []; if (success) { console.log(...msg, ...p) //document[ed.data]=null; // also delete the information } else { console.log('msg--', msg, ...p, ed); } break; } }, toolCheckFullScreen: function(doc) { if (typeof doc.fullScreen == 'boolean') return doc.fullScreen; if (typeof doc.webkitIsFullScreen == 'boolean') return doc.webkitIsFullScreen; if (typeof doc.mozFullScreen == 'boolean') return doc.mozFullScreen; return null; }, toolFormatCT: function(u) { let w = Math.round(u, 0) let a = w % 60 w = (w - a) / 60 let b = w % 60 w = (w - b) / 60 let str = ("0" + b).substr(-2) + ":" + ("0" + a).substr(-2); if (w) str = w + ":" + str return str }, loopOutwards: function(startPoint, maxStep) { let c = 0, p = startPoint, q = null; while (p && (++c <= maxStep)) { if (p.querySelectorAll('video').length !== 1) { return q; break; } q = p; p = p.parentNode; } return p || q || null; }, getActionBlockElement: function(player, layoutBox) { //player, $hs.getPlayerBlockElement(player).parentNode; //player, player.parentNode .... player.parentNode.parentNode.parentNode //layoutBox: a container element containing video and with innerHeight>=player.innerHeight [skipped wrapping] //layoutBox parentSize > layoutBox Size //actionBox: a container with video and controls //can be outside layoutbox (bilibili) //assume maximum 3 layers let outerLayout = $hs.loopOutwards(layoutBox, 3); //i.e. layoutBox.parent.parent.parent const allFullScreenBtns = $hs.queryFullscreenBtnsIndependant(outerLayout) //console.log('xx', outerLayout.querySelectorAll('[class*="-fullscreen"]').length, allFullScreenBtns.length) let actionBox = null; // console.log('fa0a', allFullScreenBtns.length, layoutBox) if (allFullScreenBtns.length > 0) { // console.log('faa', allFullScreenBtns.length) for (const possibleFullScreenBtn of allFullScreenBtns) possibleFullScreenBtn.setAttribute('__h5p_fsb__', ''); let pElm = player.parentNode; let fullscreenBtns = null; while (pElm && pElm.parentNode) { fullscreenBtns = pElm.querySelectorAll('[__h5p_fsb__]'); if (fullscreenBtns.length > 0) { break; } pElm = pElm.parentNode; } for (const possibleFullScreenBtn of allFullScreenBtns) possibleFullScreenBtn.removeAttribute('__h5p_fsb__'); if (fullscreenBtns && fullscreenBtns.length > 0) { actionBox = pElm; fullscreenBtns = $hs.exclusiveElements(fullscreenBtns); return { actionBox, fullscreenBtns }; } } let walkRes = domTool._isActionBox_1(player, layoutBox); //walkRes.elm = player... player.parentNode.parentNode (i.e. wPlayer) let parentCount = walkRes.length; if (parentCount - 1 >= 0 && domTool._isActionBox(player, walkRes, parentCount - 1)) { actionBox = walkRes[parentCount - 1].elm; } else if (parentCount - 2 >= 0 && domTool._isActionBox(player, walkRes, parentCount - 2)) { actionBox = walkRes[parentCount - 2].elm; } else { actionBox = player; } return { actionBox, fullscreenBtns: [] }; }, actionBoxRelations: {}, actionBoxMutationCallback: function(mutations, observer) { for (const mutation of mutations) { const vpid = mutation.target.getAttribute('_h5p_mf_'); if (!vpid) continue; const actionBoxRelation = $hs.actionBoxRelations[vpid]; if (!actionBoxRelation) continue; const removedNodes = mutation.removedNodes; if (removedNodes && removedNodes.length > 0) { for (const node of removedNodes) { if (node.nodeType == 1) { actionBoxRelation.mutationRemovalsCount++ node.removeAttribute('_h5p_mf_'); } } } const addedNodes = mutation.addedNodes; if (addedNodes && addedNodes.length > 0) { for (const node of addedNodes) { if (node.nodeType == 1) { actionBoxRelation.mutationAdditionsCount++ } } } } }, getActionBoxRelationFromDOM: function(elm) { //assume action boxes are mutually exclusive for (let vpid in $hs.actionBoxRelations) { const actionBoxRelation = $hs.actionBoxRelations[vpid]; const actionBox = actionBoxRelation.actionBox //console.log('ab', actionBox) if (actionBox && actionBox.parentNode) { if (elm == actionBox || actionBox.contains(elm)) { return actionBoxRelation; } } } return null; }, _actionBoxObtain: function(player) { if (!player) return null; let vpid = player.getAttribute('_h5ppid'); if (!vpid) return null; if (!player.parentNode) return null; let actionBoxRelation = $hs.actionBoxRelations[vpid], layoutBox = null, actionBox = null, boxSearchResult = null, fullscreenBtns = null, wPlayer = null; function a() { let wPlayerr= $hs.getPlayerBlockElement(player); wPlayer = wPlayerr.wPlayer; layoutBox = wPlayer.parentNode; boxSearchResult = $hs.getActionBlockElement(player, layoutBox); actionBox = boxSearchResult.actionBox fullscreenBtns = boxSearchResult.fullscreenBtns } function setDOM_mflag(startElm, endElm, vpid) { if (!startElm || !endElm) return; if (startElm == endElm) startElm.setAttribute('_h5p_mf_', vpid) else if (endElm.contains(startElm)) { let p = startElm while (p) { p.setAttribute('_h5p_mf_', vpid) if (p == endElm) break; p = p.parentNode } } } function b(domNodes) { actionBox.setAttribute('_h5p_actionbox_', vpid); if (!$hs.actionBoxMutationObserver) $hs.actionBoxMutationObserver = new MutationObserver($hs.actionBoxMutationCallback); // console.log('Major Mutation on Player Container') const actionRelation = { player: player, wPlayer: wPlayer, layoutBox: layoutBox, actionBox: actionBox, mutationRemovalsCount: 0, mutationAdditionsCount: 0, fullscreenBtns: fullscreenBtns, pContainer: domNodes[domNodes.length - 1], // the block Element as the entire player (including control btns) having size>=video ppContainer: domNodes[domNodes.length - 1].parentNode, // reference to the webpage } const pContainer = actionRelation.pContainer; setDOM_mflag(player, pContainer, vpid) for (const btn of fullscreenBtns) setDOM_mflag(btn, pContainer, vpid) setDOM_mflag = null; $hs.actionBoxRelations[vpid] = actionRelation //console.log('mutt0',pContainer) $hs.actionBoxMutationObserver.observe(pContainer, { childList: true, subtree: true }); } if (actionBoxRelation) { //console.log('ddx', actionBoxRelation.mutationCount) if (actionBoxRelation.pContainer && actionBoxRelation.pContainer.parentNode && actionBoxRelation.pContainer.parentNode === actionBoxRelation.ppContainer) { if (actionBoxRelation.fullscreenBtns && actionBoxRelation.fullscreenBtns.length > 0) { if (actionBoxRelation.mutationRemovalsCount === 0 && actionBoxRelation.mutationAdditionsCount === 0) return actionBoxRelation.actionBox // if (actionBoxRelation.mutationCount === 0 && actionBoxRelation.fullscreenBtns.every(btn=>actionBoxRelation.actionBox.contains(btn))) return actionBoxRelation.actionBox //console.log('Minor Mutation on Player Container', actionBoxRelation ? actionBoxRelation.mutationRemovalsCount : null, actionBoxRelation ? actionBoxRelation.mutationAdditionsCount : null) a(); //console.log(3535,fullscreenBtns.length) if (actionBox == actionBoxRelation.actionBox && layoutBox == actionBoxRelation.layoutBox && wPlayer == actionBoxRelation.wPlayer) { //pContainer remains the same as actionBox and layoutBox remain unchanged actionBoxRelation.ppContainer = actionBoxRelation.pContainer.parentNode; //just update the reference if (actionBoxRelation.ppContainer) { //in case removed from DOM actionBoxRelation.mutationRemovalsCount = 0; actionBoxRelation.mutationAdditionsCount = 0; actionBoxRelation.fullscreenBtns = fullscreenBtns; return actionBox; } } } } const elms = (getRoot(actionBoxRelation.pContainer) || document).querySelectorAll(`[_h5p_mf_="${vpid}"]`) for (const elm of elms) elm.removeAttribute('_h5p_mf_') actionBoxRelation.pContainer.removeAttribute('_h5p_mf_') for (var k in actionBoxRelation) delete actionBoxRelation[k] actionBoxRelation = null; delete $hs.actionBoxRelations[vpid] } if (boxSearchResult == null) a(); a = null; if (actionBox) { const domNodes = []; let pElm = player; let containing = 0; while (pElm) { domNodes.push(pElm); if (pElm === actionBox) containing |= 1; if (pElm === layoutBox) containing |= 2; if (containing === 3) { b(domNodes); b = null; return actionBox } pElm = pElm.parentNode; } } return null; // if (!actionBox.hasAttribute('tabindex')) actionBox.setAttribute('tabindex', '-1'); }, videoSrcFound: function(player) { // src loaded if (!player) return; let vpid = player.getAttribute('_h5ppid') || null; if (!vpid || !player.currentSrc) return; player._isThisPausedBefore_ = false; player.removeAttribute('_h5p_uid_encrypted'); if (player._record_continuous) player._record_continuous._lastSave = -999; //first time must save let uid_A = location.pathname.replace(/[^\d+]/g, '') + '.' + location.search.replace(/[^\d+]/g, ''); let _uid = location.hostname.replace('www.', '').toLowerCase() + '!' + location.pathname.toLowerCase() + 'A' + uid_A + 'W' + player.videoWidth + 'H' + player.videoHeight + 'L' + (player.duration << 0); digestMessage(_uid).then(function(_uid_encrypted) { let d = +new Date; let recordedTime = null; ; (function() { //read the last record only; let k3 = `_h5_player_play_progress_${_uid_encrypted}`; let k3n = `_play_progress_${_uid_encrypted}`; let m2 = Store._keys().filter(key => key.substr(0, k3.length) == k3); //all progress records for this video let m2v = m2.map(keyName => +(keyName.split('+')[1] || '0')) let m2vMax = Math.max(0, ...m2v) if (!m2vMax) recordedTime = null; else { let _json_recordedTime = null; _json_recordedTime = Store.read(k3n + '+' + m2vMax); if (!_json_recordedTime) _json_recordedTime = {}; else _json_recordedTime = jsonParse(_json_recordedTime); if (typeof _json_recordedTime == 'object') recordedTime = _json_recordedTime; else recordedTime = null; recordedTime = typeof recordedTime == 'object' ? recordedTime.t : recordedTime; if (typeof recordedTime == 'number' && (+recordedTime >= 0 || +recordedTime <= 0)) { } else if (typeof recordedTime == 'string' && recordedTime.length > 0 && (+recordedTime >= 0 || +recordedTime <= 0)) { recordedTime = +recordedTime } else { recordedTime = null } } if (recordedTime !== null) { player._h5player_lastrecord_ = recordedTime; } else { player._h5player_lastrecord_ = null; } if (player._h5player_lastrecord_ > 5) { consoleLog('last record playing', player._h5player_lastrecord_); window.setTimeout(function() { $hs._tips(player, `Press Shift-R to restore Last Playback: ${$hs.toolFormatCT(player._h5player_lastrecord_)}`, 5000, 4000) }, 1000) } })(); // delay the recording by 5.4s => prevent ads or mis operation window.setTimeout(function() { let k1 = '_h5_player_play_progress_'; let k3 = `_h5_player_play_progress_${_uid_encrypted}`; let k3n = `_play_progress_${_uid_encrypted}`; //re-read all the localStorage keys let m1 = Store._keys().filter(key => key.substr(0, k1.length) == k1); //all progress records in this site let p = m1.length + 1; for (const key of m1) { //all progress records for this video if (key.substr(0, k3.length) == k3) { Store._removeItem(key); //remove previous record for the current video p--; } } let asyncPromise = Promise.resolve(); if (recordedTime !== null) { asyncPromise = asyncPromise.then(() => { Store.save(k3n + '+' + d, jsonStringify({ 't': recordedTime })) //prevent loss of last record }) } const _record_max_ = 48; const _record_keep_ = 26; if (p > _record_max_) { //exisiting 48 records for one site; //keep only 26 records asyncPromise = asyncPromise.then(() => { const comparator = (a, b) => (a.t < b.t ? -1 : a.t > b.t ? 1 : 0); m1 .map(keyName => ({ keyName, t: +(keyName.split('+')[1] || '0') })) .sort(comparator) .slice(0, -_record_keep_) .forEach((item) => localStorage.removeItem(item.keyName)); consoleLog(`stored progress: reduced to ${_record_keep_}`) }) } asyncPromise = asyncPromise.then(() => { player.setAttribute('_h5p_uid_encrypted', _uid_encrypted + '+' + d); //try to start recording if (player._record_continuous) player._record_continuous.playingWithRecording(); }) }, 5400); }) }, bindDocEvents: function(rootNode) { if (!rootNode._onceBindedDocEvents) { rootNode._onceBindedDocEvents = true; rootNode.addEventListener('keydown', $hs.handlerRootKeyDownEvent, true) //document._debug_rootNode_ = rootNode; rootNode.addEventListener('mouseenter', $hs.handlerElementMouseEnter, true) rootNode.addEventListener('mouseleave', $hs.handlerElementMouseLeave, true) rootNode.addEventListener('mousedown', $hs.handlerElementMouseDown, true) rootNode.addEventListener('mouseup', $hs.handlerElementMouseUp, true) rootNode.addEventListener('wheel', $hs.handlerElementWheelTuneVolume, { passive: false }); // wheel - bubble events to keep it simple (i.e. it must be passive:false & capture:false) rootNode.addEventListener('focus', $hs.handlerElementFocus, $mb.eh_capture_passive()) rootNode.addEventListener('fullscreenchange', $hs.handlerFullscreenChanged, true) //rootNode.addEventListener('mousemove', $hs.handlerOverrideMouseMove, {capture:true, passive:false}) } }, fireGlobalInit: function() { if ($hs.intVideoInitCount != 1) return; if (!$hs.varSrcList) $hs.varSrcList = {}; Store.clearInvalid(_sVersion_) Promise.resolve().then(() => { GM_addStyle(` .ytp-chrome-bottom+span#volumeUI:last-child:empty{ display:none; } html[_h5p_hide_cursor]{ cursor:none !important; } `) }) }, onVideoTriggering: function() { // initialize a single video player - h5Player.playerInstance /** * 初始化播放器實例 */ let player = $hs.playerInstance if (!player) return let vpid = player.getAttribute('_h5ppid'); if (!vpid) return; let firstTime = !!$hs.initTips() if (firstTime) { // first time to trigger this player if (!player.hasAttribute('playsinline')) player.setAttribute('playsinline', 'playsinline'); if (!player.hasAttribute('x-webkit-airplay')) player.setAttribute('x-webkit-airplay', 'deny'); if (!player.hasAttribute('preload')) player.setAttribute('preload', 'auto'); //player.style['image-rendering'] = 'crisp-edges'; $hs.playbackRate = $hs.getPlaybackRate() } }, getPlaybackRate: function() { let playbackRate = Store.read('_playback_rate_') || $hs.playbackRate return Number(Number(playbackRate).toFixed(1)) }, _getPlayerBlockElement:function(player){ let layoutBox = null, wPlayer = null, wPlayerInner=null; if (!player || !player.parentNode) { return null; } /* if (useCache === true) { let vpid = player.getAttribute('_h5ppid'); let actionBoxRelation = $hs.actionBoxRelations[vpid] if (actionBoxRelation && actionBoxRelation.mutationRemovalsCount === 0) { return actionBoxRelation.wPlayer } }*/ //without checkActiveBox, just a DOM for you to append tipsDom function oWH(elm) { return [elm.offsetWidth, elm.offsetHeight].join(','); } function search_nodes() { wPlayer = player; // NOT NULL layoutBox = wPlayer.parentNode; // NOT NULL while (layoutBox.parentNode && layoutBox.nodeType == 1 && layoutBox.offsetHeight == 0) { wPlayer = layoutBox; // NOT NULL layoutBox = layoutBox.parentNode; // NOT NULL } //container must be with offsetHeight while (layoutBox.parentNode && layoutBox.nodeType == 1 && layoutBox.offsetHeight < player.offsetHeight) { wPlayer = layoutBox; // NOT NULL layoutBox = layoutBox.parentNode; // NOT NULL } //container must have height >= player height wPlayerInner=wPlayer const layoutOWH = oWH(layoutBox) //const playerOWH=oWH(player) //skip all inner wraps while (layoutBox.parentNode && layoutBox.nodeType == 1 && oWH(layoutBox.parentNode) == layoutOWH) { wPlayer = layoutBox; // NOT NULL layoutBox = layoutBox.parentNode; // NOT NULL } // oWH of layoutBox.parentNode != oWH of layoutBox and layoutBox.offsetHeight >= player.offsetHeight } search_nodes(); if (layoutBox.nodeType == 11) { makeNoRoot(layoutBox); search_nodes(); } //condition: //!layoutBox.parentNode || layoutBox.nodeType != 1 || layoutBox.offsetHeight > player.offsetHeight // layoutBox is a node contains