Greasy Fork is available in English.

HTML5 Video Player Enhance

To enhance the functionality of HTML5 Video Player (h5player) supporting all websites using shortcut keys similar to PotPlayer.

Verze ze dne 15. 06. 2021. Zobrazit nejnovější verzi.

// ==UserScript==
// @name         HTML5 Video Player Enhance
// @version      2.9.0a4
// @description  To enhance the functionality of HTML5 Video Player (h5player) supporting all websites using shortcut keys similar to PotPlayer.
// @author       CY Fung
// @match        http://*/*
// @match        https://*/*
// @run-at       document-start
// @require https://cdnjs.cloudflare.com/ajax/libs/js-sha256/0.9.0/sha256.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/mathjs/9.3.2/math.js
// @namespace https://greatest.deepsurf.us/users/371179
// @grant   GM_getValue
// @grant   GM_setValue
// ==/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 $$($hs) {
    'use strict';

    if (!document || !document.documentElement) return window.requestAnimationFrame($$);

    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 _ell_timeupdatefs = [];
    let _endlessloop = null;
    const isIframe = (window.top !== window.self && window.top && window.self);
    window.__MutationObserver = window.MutationObserver || window.WebKitMutationObserver || null;
    const shadowRoots = [];

    const getRoot = (elm) => elm.getRootNode instanceof Function ? elm.getRootNode() : (elm.ownerDocument || null);

    const isShadowRoot = (elm) => (elm && ('host' in elm)) ? elm.nodeType == 11 && !!elm.host && elm.host.nodeType == 1 : null; //instanceof ShadowRoot


    if (!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
    }

    Element.prototype.__requestPointerLock = Element.prototype.requestPointerLock ||
        Element.prototype.mozRequestPointerLock || Element.prototype.webkitRequestPointerLock || function() {};

    // Ask the browser to release the pointer
    Document.prototype.__exitPointerLock = Document.prototype.exitPointerLock ||
        Document.prototype.mozExitPointerLock || Document.prototype.webkitExitPointerLock || function() {};

    //  built-in hash - https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
    async function digestMessage(message) {
        return window.sha256(message)
    }

    var $ws = {
        requestAnimationFrame,
        cancelAnimationFrame
    }
    //throw Error if your browser is too outdated. (eg ES6 script, no such window object)



    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 {
                var 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;
        var a = 0;
        while (win = win.parent) {
            _postMsg.a = ++a;
            _postMsg(win, ...arguments)
            if (win == top) break;
        }
    }



    function lowerKeyCode(keyCode) {
        if (keyCode >= 65 && keyCode <= 90) keyCode += 32;
        return keyCode
    }

    function whichTransitionEvent(type) {
        if (whichTransitionEvent['_result_' + type]) return whichTransitionEvent['_result_' + type]
        var 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 (whichTransitionEvent['_result_' + type] = transitions[styleProp]);
            }
        }
    }

    class ResizeODM {
        static __init__() {
            this.__resizerCount__ = 0;
            this.__resizeListeners__ = {};
        }
        constructor() {
            let rm = this;
            ResizeODM.__resizerCount__++;
            let rpid = "rpid-" + ResizeODM.__resizerCount__;
            ResizeODM.__resizeListeners__[rpid] = [];

            rm._resizer_listeners = ResizeODM.__resizeListeners__[rpid];

            var odm = document.createElement('object');
            odm.setAttribute('_resizer_odm_', rpid);
            odm.setAttribute('style', 'display: block; position: absolute; top: -300vh; left: -300vw; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;');
            odm.onload = ResizeODM.objectLoad;
            odm.type = 'text/html';
            odm.data = 'about:blank';
            rm.odm = odm;
            rm.rpid = rpid;
            rm.odm.__rm__ = rm;
        }

        static find(rsElm) {
            if (!rsElm) return null;
            let odm = [...rsElm.querySelectorAll('object[_resizer_odm_]')].filter(elm => elm.parentNode == rsElm)[0];
            if (!odm) return null;

            return odm.__rm__ || null;

        }

        static resizeListener(e) {
            let odv = e.target || e.srcElement;
            let rm = odv._resizer_rm;
            if (rm.__resizeRAF__) $ws.cancelAnimationFrame(rm.__resizeRAF__);
            rm.__resizeRAF__ = $ws.requestAnimationFrame(function() {
                rm.__resizeRAF__ = 0;
                for (const fn of rm._resizer_listeners) fn.call(rm, e);
            });
        }

        static objectLoad(e) {
            let odm = this;
            let rm = odm.__rm__;
            let odv = odm.contentDocument.defaultView
            odv._resizer_rm = rm;
            rm.odv = odv;
            odv.onresize = ResizeODM.resizeListener;
        }

        resizeElement() {
            return this.odm.parentNode;
        }

        // ResizeODM.relativeParent(rsElm);

        relativeParent(rsElm, existingnode = null) {
            let odm = this.odm;
            rsElm = rsElm || odm.parentNode;
            let rpid = this.rpid;
            //if (getComputedStyle(rsElm).position == 'static') rsElm.style.position = 'relative';
            rsElm.insertBefore(odm, existingnode);
        }

        listen(fn) {
            this._resizer_listeners.push(fn);
        }

        unlisten(fn) {
            this._resizer_listeners.splice(this._resizer_listeners.indexOf(fn), 1);
        }

        remove() {
            this._resizer_listeners.length = 0;
            this.odv.onresize = null;
            this.odm.onload = null;
        }

    }

    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;
    }

    class SimEvent extends Event {
        constructor(typeArg, eventInit, customType) {
            super(typeArg, eventInit);
            this.__customType = customType;
        }
        get type() {
            return this.__customType;
        }
    }

    const _evtOB_create = function(_EVT, d) {
        let keys = Object.keys(_EVT.prototype);
        let res = function Event(m) {
            let o = this
            for (const k of keys) {
                if (k in m && !(k in o)) o[k] = m[k];
            }
            Object.assign(o, d);
        }
        return res
    }
    _evtOB_create._timeupdate = function() {
        if (_evtOB_create.__timeupdate) return _evtOB_create.__timeupdate;
        let res = _evtOB_create(Event, {
            isTrusted: true,
            type: 'timeupdate'
        })
        return _evtOB_create.__timeupdate = res;
    }
    const sim_arg_1_fn = function(f, evtOB) {
        return function(e) {
            let o = new evtOB(e);
            let arg = [...arguments];
            arg[0] = o;
            return f.apply(this, arg);
        }
    }

    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
    };

    class EndlessLoop {
        constructor() {
            this.activeLoopsCount = 0;
            this.loops = [];
            this.cid = 0;
            this._loop = () => {
                if (!this.cid) return; //cancelled
                for (const loop of this.loops) {
                    if (loop.opt.looping) loop.fn(loop.opt);
                }
                this.cid = $ws.requestAnimationFrame(this._loop);
            }
        }

        loopStart() {
            this.looping = true;
            this.cid = $ws.requestAnimationFrame(this._loop);
        }
        loopStop() {
            if (this.cid) $ws.cancelAnimationFrame(this.cid);
            this.cid = 0;
            this.looping = false;
        }
        append(fn) {
            const opt = new EndlessLoopOpts(this);
            this.loops.push({
                fn,
                opt
            });
            return opt;
        }
    }

    class EndlessLoopOpts {
        constructor(ell) {
            this._looping = false;
            this._ell = ell;
        }
        _loop() {
            this._ell.loops.some(loop => loop.opt === this && loop.opt.looping ? (loop.fn(loop.opt), true) : null);
        }
        get looping() {
            return this._looping;
        }
        loopingStart() {
            if (this._looping === false) {
                this._looping = true;
                this._ell.activeLoopsCount++;
                if (this._ell.activeLoopsCount == 1) this._ell.loopStart();
            }
        }
        loopingStop() {
            if (this._looping === true) {
                this._looping = false;
                this._ell.activeLoopsCount--;
                if (this._ell.activeLoopsCount == 0) this._ell.loopStop();
            }
        }
    }

    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);
    }

    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) {
                Object.keys(localStorage)
                    .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;

        }

    }

    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) {

            let vElCSS = domTool.cssWH(vEl);
            let vElCSSw = vElCSS.w;
            let vElCSSh = vElCSS.h;

            let vElx = vEl;
            let 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) {
                let 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
                }
            }

            let parentCount = walkRes.length;
            if (pEl_idx >= 0 && pEl_idx < parentCount) {} else {
                return;
            }
            let pElr = walkRes[pEl_idx]
            if (!pElr.css) {
                //shadowRoot
                return true;
            }

            let pEl = pElr.elm;

            //prevent activeElement==body
            let pElCSS = domTool.cssWH(pEl, pElr.css);

            //check prediction of parent dimension
            let d1v = absDiff(pElCSS.w, pElr.padW, pElCSS.h, pElr.padH)

            let d1x = d1v[0] < 10
            let 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

            let pEl_rect = pEl.getBoundingClientRect()
            let vEl_rect = vEl.getBoundingClientRect()

            let pEl_center = midPoint(pEl_rect)
            let vEl_center = midPoint(vEl_rect)

            let d2v = absDiff(pEl_center.x, vEl_center.x, pEl_center.y, vEl_center.y);

            let d2x = d2v[0] < 10;
            let 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 = _doc.querySelector('head') || _doc.querySelector('html') || _doc;
                }
                let doc = head.ownerDocument;
                let style = doc.createElement('style');
                style.type = 'text/css';
                let node = doc.createTextNode(css);
                style.appendChild(node);
                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) {
                $ws.requestAnimationFrame(function() {
                    dom.style.opacity = 0;
                    dom.style.transform = 'translate(-9999px)';
                    dom = null;
                })
            }
        }
    };

    const handle = {

        timeupdatef_ell: async function(opts) {
            let video = opts.video;
            let time = video.currentTime;
            if (time !== opts.lastTime) {
                opts.lastTime = time;
                video.dispatchEvent(opts.evt);
            }
        },
        playbackELL: async (opts) => {

            let qTime = +new Date;
            if (qTime >= opts.pTime) {
                opts.pTime = qTime + opts.timeDelta; //prediction of next Interval
                opts.playbackRecord()
            }

        },
        playbackRecord: async 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

                //console.log('aasas',this.player_uid, shallSave, '_play_progress_'+_uid, currentTimeToSave)

                Store.save('_play_progress_' + _uid, jsonStringify({
                    't': currentTimeToSave
                }))

            }

        },
        pr_updateUID: function() {

            //this refer to endless's opts

            let player = this.player;

            let _uid = player.getAttribute('_h5p_uid_encrypted') || ''
            if (!_uid) return false;
            this.player_uid = _uid;

            return true;

        },

    };

    class VideoListener {
        usable = false;
        observer = null;
        asyncCheckFunc = null;
        cid_asyncCheckFunc = 0;

        constructor(shadowRoot) {
            this.rootElement = shadowRoot || window.document;
            if (this.rootElement && !this.rootElement.__videoListenerEnabled__) {
                this.rootElement.__videoListenerEnabled__ = true;
                this.usable = true;
            }
        }

        delayCheck() {
            if (this.cid_asyncCheckFunc) $ws.cancelAnimationFrame(this.cid_asyncCheckFunc);
            this.cid_asyncCheckFunc = $ws.requestAnimationFrame(this.asyncCheckFunc);
        }

        listen(fn) {
            if (!this.usable) return;
            this.init_observer();
            this.fn = fn; // single function
            this.checkOnce();
            this.checkEach();
        }
        init_observer() {
            if (this.observer) return;
            this.asyncCheckFunc = () => this.checkEach();
            this.observer = new window.__MutationObserver(this.mutationObserverCallback);
            this.observer.observe(this.rootElement, {
                childList: true,
                subtree: true
            })
            this.observer.videoListener = this;
        }
        mutationObserverCallback(mutations, observer) {


            if (mutations && 'length' in mutations) {
                var requireChecking = Array.prototype.some.call(mutations, mutation => {
                    let addedNodes = mutation.addedNodes;
                    if (!addedNodes || !addedNodes.length) return false;
                    return Array.prototype.some.call(addedNodes, addedNode => {
                        return (addedNode.nodeName == 'VIDEO' || addedNode.childElementCount > 0)
                    });
                });

                if (requireChecking) observer.videoListener.delayCheck();

            }

        }

        async checkOnce() {
            var treeWalker = document.createTreeWalker(
                document.documentElement,
                NodeFilter.SHOW_ELEMENT, {
                    acceptNode: (node) => (node.shadowRoot ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP)
                }
            );
            var nodeList = [];
            while (treeWalker.nextNode()) nodeList.push(treeWalker.currentNode);
            for (const node of nodeList) this._checkRoot(node.shadowRoot);
        }

        async checkEach() {
            this._checkRoot(this.rootElement);
        }
        _checkRoot(shadowRoot) {
            const videos = shadowRoot.querySelectorAll('VIDEO'); //shadowRoot don't have getElementsByTagName
            for (const video of videos) {
                if (!video._onceMutationReady) {
                    video._onceMutationReady = true;
                    video.setAttribute('h5p_observed', '1');
                    this.fn(video);
                }
            }
        }
    }
    const _keyMap = function(arr, obj) {
        let res = {};
        for (const key of arr) res[key] = key.charCodeAt(0);
        Object.assign(res, obj);
        return res;
    }


    $hs = {

        pictureInPicture: function(videoElm) {
            if (document.pictureInPictureElement) {
                document.exitPictureInPicture();
            } else if ('requestPictureInPicture' in videoElm) {
                videoElm.requestPictureInPicture()
            } else {
                $hs.tips('PIP is not supported.');
            }

        },

        handlerVideoPlaying: function(evt) {

            $hs._actionBoxSet(evt.target);
            $hs.playerInstance = evt.target;
            $hs.onVideoTriggering();

            if (!$hs.enable) return $hs.tips(false);
            let player = this;
            if (player._isThisPausedBefore_) consoleLog('resumed')
            let _pausedbefore_ = player._isThisPausedBefore_

            if (player.playpause_cid) {
                clearTimeout(player.playpause_cid);
                player.playpause_cid = 0;
            }
            let _last_paused = player._last_paused
            player._last_paused = player.paused
            if (_last_paused === !player.paused) {
                player.playpause_cid = setTimeout(() => {
                    if (player.paused === !_last_paused && !player.paused && _pausedbefore_) {
                        $hs.tips('Playback resumed', undefined, 2500)
                    }
                }, 90)
            }

            /* 播放的時候進行相關同步操作 */

            if (!player._record_continuous) {

                /* 同步之前設定的播放速度 */
                $hs.setPlaybackRate()

                if (!_endlessloop) _endlessloop = new EndlessLoop();

                player._record_continuous = _endlessloop.append(handle.playbackELL);
                player._record_continuous._lastSave = -999;

                player._record_continuous.timeDelta = 2000;
                player._record_continuous.player = player
                player._record_continuous.playbackRecord = handle.playbackRecord;
                player._record_continuous.updateUID = handle.pr_updateUID;

                player._record_continuous.playingWithRecording = function() {
                    let player = this.player;
                    if (!player.paused && this.updateUID() && !this.looping) {
                        this.pTime = 0;
                        this.loopingStart();
                    }
                }

            }

            player._record_continuous.playingWithRecording(player); //try to start recording

            for (const opts of _ell_timeupdatefs) opts.loopingStart();

            player._isThisPausedBefore_ = false;

        },
        handlerVideoPause: function() {

            if (!$hs.enable) return $hs.tips(false);
            let player = this;
            consoleLog('pause')
            player._isThisPausedBefore_ = true;

            let _last_paused = player._last_paused
            player._last_paused = player.paused
            if (player.playpause_cid) {
                clearTimeout(player.playpause_cid);
                player.playpause_cid = 0;
            }
            if (_last_paused === !player.paused) {
                player.playpause_cid = setTimeout(() => {
                    if (player.paused === !_last_paused && player.paused) {
                        $hs.tips('Playback paused', undefined, 2500)
                    }
                }, 90)
            }


            if (player._record_continuous && player._record_continuous.looping) {
                player._record_continuous.playbackRecord(); //playbackRecord once before stopping  //handle.playbackRecord;
                player._record_continuous.loopingStop();
            }

            for (const opts of _ell_timeupdatefs) opts.loopingStop();

        },
        handlerVideoVolumeChange: function() {

            if (this.volume >= 0) {} else {
                return;
            }
            let cVol = this.volume;
            let cMuted = this.muted;

            if (cVol === this._volume_p && cMuted === this._muted_p) {
                // nothing changed
            } else if (cVol === this._volume_p && cMuted !== this._muted_p) {
                // muted changed
            } else { // cVol != pVol

                // only volume changed

                let shallShowTips = this._volume >= 0; //prevent initialization

                if (!cVol) {
                    this.muted = true;
                } else if (cMuted) {
                    this.muted = false;
                    this._volume = cVol;
                } else if (!cMuted) {
                    this._volume = cVol;
                }
                consoleLog('volume changed')

                let player = this;

                if (shallShowTips)
                    $hs.tips('Volume: ' + dround(player.volume * 100) + '%', undefined, 3000)

            }

            this._volume_p = cVol
            this._muted_p = cMuted

        },
        handlerVideoLoadedMetaData: function() {
            consoleLog('video size', this.videoWidth + ' x ' + this.videoHeight);

            let player = this
            let vpid = player.getAttribute('_h5ppid') || null;
            if (!vpid || !player.currentSrc) return;

            if ($hs.varSrcList[vpid] != player.currentSrc) {
                $hs.varSrcList[vpid] = player.currentSrc;
                $hs.videoSrcFound(player);
                $hs._actionBoxSet(player);
            }

            if (!player._onceVideoLoaded) {
                player._onceVideoLoaded = true;
                player.addEventListener('playing', $hs.handlerVideoPlaying)
                player.addEventListener('pause', $hs.handlerVideoPause);
                player.addEventListener("volumechange", $hs.handlerVideoVolumeChange);
            }

        },
        handlerElementMouseEnter: function(evt) {
            if ($hs.intVideoInitCount > 0) {} else {
                return;
            }
            var videoActive = $hs.trigger_actionBoxes.some(actionBox => (evt.target === actionBox || actionBox.contains(evt.target)))
            $hs.debug01(evt,videoActive)
            if (!videoActive) return;


            let player = evt.target;
            if (player.nodeName != "VIDEO") player = player.querySelector('video[_h5ppid]');
            if (player) $hs._actionBoxSet(player);
        },
        handlerElementMouseDown: function(evt) {

            if ($hs.intVideoInitCount > 0) {} else {
                return;
            }
            var videoActive = $hs.trigger_actionBoxes.some(actionBox => (evt.target === actionBox || actionBox.contains(evt.target)))
            $hs.debug01(evt,videoActive)
            if (!videoActive) return;



            let player = this; //evt.target may be something else
            if (player.nodeName != "VIDEO") player = player.querySelector('video[_h5ppid]');
            if (!player) return;
            let {
                layoutBox,
                wPlayer
            } = $hs.getLayoutBox(player);
            if (layoutBox && layoutBox.contains(evt.target)) {
                $hs.makeFocus(player, evt)
            }
        },
        handlerElementWheelTuneVolume: function(evt) { //shift + wheel

            if ($hs.intVideoInitCount > 0) {} else {
                return;
            }
            var videoActive = $hs.trigger_actionBoxes.some(actionBox => (evt.target === actionBox || actionBox.contains(evt.target)))
            $hs.debug01(evt,videoActive)
            if (!videoActive) return;
            if (!evt.shiftKey) return;
            if (evt.deltaY) {
                let player = $hs.player();
                if (!player) return;

                if (evt.deltaY > 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

                } else if (evt.deltaY < 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

                }
            }
        },
        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')
        },
        handlerElementDblClick: function(evt) {

            if ($hs.intVideoInitCount > 0) {} else {
                return;
            }
            var videoActive = $hs.trigger_actionBoxes.some(actionBox => (evt.target === actionBox || actionBox.contains(evt.target)))
            $hs.debug01(evt,videoActive)
            if (!videoActive) return;

            consoleLog('dblclick', this, evt.target)

            if (this.nodeName == 'VIDEO' && evt.target.nodeName == 'VIDEO' && this.getAttribute('_h5p_actionbox_')) {
                return;
            }
            if (document.readyState != "complete") return;

            var actionBox = $hs.trigger_actionBoxes.filter(actionBox => (evt.target === actionBox || actionBox.contains(evt.target)))[0]


            let vpid = actionBox.getAttribute('_h5p_actionbox_');
            if (!vpid) return;
            let player = getRoot(actionBox).querySelector(`[_h5ppid="${vpid}"]`);
            $hs._actionBoxSet(player)
            $hs.playerInstance = player
            $hs.onVideoTriggering()

            $hs.callFullScreenBtn();

            evt.stopPropagation()
            evt.preventDefault()
            return false

        },
        handlerDocFocusOut: function(e) {
            let doc = this;
            $hs.focusFxLock = true;
            $ws.requestAnimationFrame(function() {
                $hs.focusFxLock = false;
                if (!$hs.enable) $hs.tips(false);
                else
                if (!doc.hasFocus() && $hs.player() && !$hs.isLostFocus) {
                    $hs.isLostFocus = true;
                    consoleLog('doc.focusout')
                    //$hs.tips('focus is lost', -1);
                }
            });
        },
        handlerDocFocusIn: function(e) {
            let doc = this;
            if ($hs.focusFxLock) return;
            $ws.requestAnimationFrame(function() {

                if ($hs.focusFxLock) return;
                if (!$hs.enable) $hs.tips(false);
                else
                if (doc.hasFocus() && $hs.player() && $hs.isLostFocus) {
                    $hs.isLostFocus = false;
                    consoleLog('doc.focusin')
                    $hs.tips(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;
            switch (tag) {
                case 'consoleLog':
                    var msg_str = ed.str;
                    var 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
                    }
                    var 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;

            }
        },

        isInActiveMode: function(activeElm, player) {

            if (activeElm == player) {
                return true;
            }

            let _checkingPass = false;
            let {
                layoutBox,
                wPlayer
            } = $hs.getLayoutBox(player);
            if (layoutBox && layoutBox.parentNode && layoutBox.contains(activeElm)) {
                let rpid = player.getAttribute('_h5ppid') || "NULL";
                let actionBox = layoutBox.parentNode.querySelector(`[_h5p_actionbox_="${rpid}"]`); //the box can be layoutBox
                if (actionBox && actionBox.contains(activeElm)) _checkingPass = true;
            }

            return _checkingPass
        },


        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

        },

        /* 提示文本的字號 */
        fontSize: 16,
        enable: true,
        playerInstance: null,
        translate: {
            x: 0,
            y: 0
        },
        playbackRate: 1,
        /* 快進快退步長 */
        skipStep: 5,
        /* 獲取當前播放器的實例 */
        player: function() {
            return $hs.playerInstance || $hs.getPlayerList()[0]
        },
        /* 每個網頁可能存在的多個video播放器 */
        getPlayerList: function() {
            let list = [...document.querySelectorAll('video')];
            for (const shadowRoot of shadowRoots) list.push(...shadowRoot.querySelectorAll('video'));
            return list
        },
        getPlayerWrapDom: function() {
            let player = $hs.player()
            if (!player || !player.getBoundingClientRect) return
            let wrapDom = null
            let layoutBox = player.getBoundingClientRect()
            if (layoutBox.width && layoutBox.height) {
                domTool.eachParentNode(player, function(parent) {
                    if (parent === document || !parent.getBoundingClientRect) return
                    let parentBox = parent.getBoundingClientRect()
                    if (parentBox.width === layoutBox.width && parentBox.height === layoutBox.height) {
                        wrapDom = parent
                    }
                })
            }
            return wrapDom
        },
        makeFocus: function(player, evt) {
            setTimeout(function() {
                let rpid = player.getAttribute('_h5ppid');
                let actionBox = getRoot(player).querySelector(`[_h5p_actionbox_="${rpid}"]`);

                //console.log('p',rpid, player,actionBox,document.activeElement)
                if (actionBox && actionBox != document.activeElement && !actionBox.contains(document.activeElement)) {
                    consoleLog('make focus on', actionBox)
                    actionBox.focus();

                }

            }, 300)
        },

        trigger_actionBoxes: [],

        _actionBoxSet: function(player) {

            if (!player) return null;
            let vpid = player.getAttribute('_h5ppid');
            if (!vpid) return null;

            let {
                layoutBox,
                wPlayer
            } = $hs.getLayoutBox(player)

            if (!layoutBox) return null;

            let walkRes = domTool._isActionBox_1(player, layoutBox);
            let parentCount = walkRes.length;
            let actionBox = null;
            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;
            }
            if (actionBox && actionBox.getAttribute('_h5p_actionbox_') != vpid) {
                consoleLog('D-1', actionBox);
                for (const previousActionboxes of getRoot(player).querySelectorAll(`[_h5p_actionbox_="${vpid}"]`)) {
                    //change of parentNode
                    previousActionboxes.removeAttribute('_h5p_actionbox_');


                }
                actionBox.setAttribute('_h5p_actionbox_', vpid);
                $hs.trigger_actionBoxes = [actionBox];

                if (!actionBox.hasAttribute('tabindex')) actionBox.setAttribute('tabindex', '-1');



            }
            return actionBox;
        },

        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 k1 = '_h5_player_play_progress_';
                    let k1n = '_play_progress_';
                    let k2 = _uid_encrypted;
                    let k3 = k1 + k2;
                    let k3n = k1n + k2;
                    let m2 = Object.keys(localStorage).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_);
                        setTimeout(function() {
                            let tmp_player = $hs.playerInstance;
                            $hs.playerInstance = player;
                            $hs.tips(`Press Shift-R to restore Last Playback: ${$hs.toolFormatCT(player._h5player_lastrecord_)}`, 5000, 4000)
                            $hs.playerInstance = tmp_player
                        }, 1000)
                    }

                })();
                // delay the recording by 5.4s => prevent ads or mis operation
                setTimeout(function() {

                    let k1 = '_h5_player_play_progress_';
                    let k1n = '_play_progress_';
                    let k2 = _uid_encrypted;
                    let k3 = k1 + k2;
                    let k3n = k1n + k2;

                    //re-read all the localStorage keys
                    let m1 = Object.keys(localStorage).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) {
                            localStorage.removeItem(key); //remove previous record for the current video
                            p--;
                        }
                    }

                    if (recordedTime !== null) {
                        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

                        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_}`)
                    }

                    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('mousedown', $hs.handlerElementMouseDown, true)
                rootNode.addEventListener('dblclick', $hs.handlerElementDblClick, true)
                rootNode.addEventListener('wheel', $hs.handlerElementWheelTuneVolume, {
                    passive: false
                });
                // wheel - bubble events to keep it simple (i.e. it must be passive:false & capture:false)

            }
        },
        fireGlobalInit: function() {

            /* 綁定鍵盤事件 */
            if ($hs.intVideoInitCount != 1) return;

            if (!$hs.varSrcList) $hs.varSrcList = {};

            $hs.isLostFocus = null;

            try {
                //iframe may not be able to control top window
                //error; just ignore with asycn
                let topDoc = window.top && window.top.document ? window.top.document : null;
                if (topDoc) {
                    topDoc.addEventListener('focusout', $hs.handlerDocFocusOut, true)
                    topDoc.addEventListener('focusin', $hs.handlerDocFocusIn, true)
                }

            } catch (e) {}

            Store.clearInvalid(_sVersion_)


        },
        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.filter.reset()
                $hs.playbackRate = $hs.getPlaybackRate()
            }

        },
        getPlaybackRate: function() {
            let playbackRate = Store.read('_playback_rate_') || $hs.playbackRate
            return Number(Number(playbackRate).toFixed(1))
        },
        getLayoutBox: function(player, skipPlayer) {
            //without checkActiveBox, just a DOM for you to append tipsDom

            var layoutBox = null,
                wPlayer = null

            if (!player || !player.offsetHeight || !player.offsetWidth) {
                return {
                    layoutBox,
                    wPlayer
                };
            }

            if (!player.parentNode) {
                return {
                    layoutBox,
                    wPlayer
                };
            }

            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
                }
                while (layoutBox.parentNode && layoutBox.nodeType == 1 && layoutBox.offsetHeight < player.offsetHeight) {
                    wPlayer = layoutBox; // NOT NULL
                    layoutBox = layoutBox.parentNode; // NOT NULL
                }

            }

            search_nodes();

            if (layoutBox.nodeType == 11) {


                //shadowRoot without html and body
                let shadowChild, shadowElm_container, shadowElm_head, shadowElm_html;
                let rootNode = getRoot(player);
                if (rootNode.querySelectorAll('html,body').length < 2) {

                    shadowElm_container = player.ownerDocument.createElement('BODY')
                    rootNode.insertBefore(shadowElm_container, rootNode.firstChild)

                    while (shadowChild = shadowElm_container.nextSibling) shadowElm_container.appendChild(shadowChild);

                    shadowElm_head = rootNode.insertBefore(player.ownerDocument.createElement('HEAD'), shadowElm_container)

                    shadowElm_html = rootNode.insertBefore(player.ownerDocument.createElement('HTML'), shadowElm_head)

                    shadowElm_container.setAttribute('style', 'padding:0;margin:0;border:0;    box-sizing: border-box;')
                    shadowElm_html.setAttribute('style', 'padding:0;margin:0;border:0;    box-sizing: border-box;')

                    shadowElm_html.appendChild(shadowElm_head)
                    shadowElm_html.appendChild(shadowElm_container)

                }

                search_nodes();

            }

            //condition:
            //!layoutBox.parentNode || layoutBox.nodeType != 1 || layoutBox.offsetHeight > player.offsetHeight

            // layoutBox is a node contains <video> and offsetHeight>=video.offsetHeight
            // wPlayer is a HTML Element (nodeType==1)
            // you can insert the DOM element into the layoutBox

            if (layoutBox.nodeType !== 1) {
                //unexpected
                layoutBox = null;
                wPlayer = null;
            }

            return {
                layoutBox,
                wPlayer
            };

        },
        toParentContains: function(layoutBox, selector) {

            let r = layoutBox
            while (r && r.nodeType == 1 && !r.querySelector(selector)) r = r.parentNode;
            r = r || layoutBox;
            if (r && r.nodeType == 1) {
                return r;
            }

            return null;
        },
        getPlayerCTBox: function(elm1, elm2) {

            let box1 = elm1;
            let box2 = elm2;

            while (box1 && box2) {
                if (box1.contains(box2) || box2.contains(box1)) {
                    break;
                }
                box1 = box1.parentNode;
                box2 = box2.parentNode;
            }

            let layoutBox = null;

            box1 = (box1 && box1.contains(elm2)) ? box1 : null;
            box2 = (box2 && box2.contains(elm1)) ? box2 : null;

            if (box1 && box2) layoutBox = box1.contains(box2) ? box2 : box1;
            else layoutBox = box1 || box2 || null;

            return layoutBox

        },
        change_layoutBox: function(tipsDom) {
            let player = $hs.player()
            let {
                layoutBox,
                wPlayer
            } = $hs.getLayoutBox(player)

            if (wPlayer && layoutBox && wPlayer.parentNode == layoutBox) {} else {
                //unexpected
                return;
            }
            //console.log(layoutBox,wPlayer)
            //console.log('cp',layoutBox)
            if ((layoutBox && layoutBox.nodeType == 1) && (!tipsDom.parentNode || tipsDom.parentNode !== layoutBox)) {

                consoleLog('changed_layoutBox')
                layoutBox.insertBefore(tipsDom, wPlayer);

                if (tipsDom._vpr && tipsDom._vpr_obj && tipsDom._vpr_pn) {
                    let rm = ResizeODM.find(tipsDom._vpr_pn)
                    if (rm) {
                        rm.unlisten(tipsDom._vpr)
                        tipsDom._vpr_pn = tipsDom.parentNode
                        rm.relativeParent(layoutBox, wPlayer)
                        rm.listen(tipsDom._vpr)
                        rm.odm.id = tipsDom._vpr_obj.id
                        tipsDom._vpr_obj = rm.odm;
                    } else {
                        tipsDom._vpr_pn = tipsDom.parentNode
                    }

                }

            }
        },
        callFullScreenBtn0: function() {
            return this.setWebFullScreen();
        },
        getPlayerTips: function() {
            let player = $hs.player()
            let tcn = player.getAttribute('_h5player_tips') || ($hs.tipsClassName);
            return getRoot(player).querySelector('#' + tcn)
        },
        setWebFullScreen: function() {
            let player = $hs.player()
            let tipsDom = $hs.getPlayerTips();

            let {
                layoutBox,
                wPlayer
            } = $hs.getLayoutBox(player);

            let rpid = player.getAttribute('_h5ppid') || "NULL";
            let gPlayer = null;
            if (layoutBox && layoutBox.parentNode) {
                gPlayer = layoutBox.parentNode.querySelector(`[_h5p_actionbox_="${rpid}"]`); //the box can be layoutBox
            }

            if (gPlayer) {
                consoleLog('gPlayer', gPlayer)

                let chFull = $hs.toolCheckFullScreen(gPlayer.ownerDocument);

                if (chFull === true) {
                    consoleLog('chFull', 'true')
                    player.ownerDocument.exitFullscreen();
                } else {
                    consoleLog('chFull', 'false')
                    consoleLog('DOM fullscreen')
                    try {
                        gPlayer.requestFullscreen() //known bugs : TypeError: fullscreen error
                    } catch (e) {
                        consoleLogF('fullscreen error', e)
                    }
                }
            } else {
                consoleLog('the container for DOM fullscreen cannot be found')
            }

        },
        callFullScreenBtn: function() {
            let player = $hs.player()
            if (!player || !player.ownerDocument) return this.callFullScreenBtn0();
            let tcn = player.getAttribute('_h5player_tips') || ($hs.tipsClassName);
            consoleLog('tcn', tcn)
            let tipsDom = getRoot(player).querySelector('#' + tcn)
            if (!tipsDom) return this.callFullScreenBtn0();
            let chFull = $hs.toolCheckFullScreen(player.ownerDocument);
            if (chFull === null) return (console.log('chFull', 'null'), this.callFullScreenBtn0());
            if (chFull === true) {
                consoleLog('chFull', 'true')
                player.ownerDocument.exitFullscreen();
                //  let el = [t.player_focus_input]; if (el) {el = el[0].click()}
            } else {
                consoleLog('chFull', 'false')

                const layoutBox = $hs.getPlayerCTBox(player, tipsDom);

                let pPlayer = player
                let qPlayer = layoutBox
                let clicked = false;
                let gs1;
                let _gs1_tmp = {};
                let _gs1_filter = function(elq) {
                    return _gs1_tmp.elm == elq.elm ? false : _gs1_tmp.elm.contains(elq.elm)
                };
                // try to find the fullscreen button
                for (let q3 = 0; q3 < 4; q3++) { //max 4 layers
                    if (!qPlayer) break;
                    let fs1 = qPlayer.querySelectorAll('[class*="fullscreen"]')
                    if (fs1.length > 0) {
                        // -- indiv-elm --
                        gs1 = Array.prototype.map.call(fs1, function(elm) {
                            return {
                                elm: elm,
                                isVisible: null,
                                hasClickListeners: null,
                                childElementCount: null,
                                isContained: null
                            }
                        });
                        if (('_listeners' in document)) {
                            for (const elp of gs1) {
                                let elm = elp.elm;
                                elp.hasClickListeners = elm._listeners && elm._listeners.click && elm._listeners.click.funcCount > 0
                            }
                        }
                        if ('childElementCount' in player) {
                            for (const elp of gs1) {
                                let elm = elp.elm;
                                elp.childElementCount = elm.childElementCount;
                            }
                        }
                        if ('offsetParent' in player) {
                            for (const elp of gs1) {
                                let elm = elp.elm;
                                elp.isVisible = !!elm.offsetParent; //works with parent/self display none; not work with visiblity hidden / opacity0
                            }
                        }
                        gs1 = gs1.filter(function(elp) {
                            return elp.hasClickListeners
                        })
                        //console.log('gs1',gs1)
                        // -- inter-elm --
                        if ('contains' in player) {
                            for (const elp of gs1) {

                                if (elp.childElementCount === 0) return false;
                                _gs1_tmp.elm = elp.elm;
                                elp.isContained = gs1.filter(_gs1_filter).length > 0;

                            }

                        }
                        let gs2 = gs1.filter(function(elp) {
                            return !elp.isContained && elp.isVisible
                        })
                        consoleLog('fullscreen btn', gs2)
                        //console.log('gs2',gs2)
                        if (gs2.length >= 1) {
                            let gs2_a = gs2.map(elp => elp.elm.className.length)
                            let gs2_b = Math.min.apply(Math, gs2_a)
                            let gs2_c = gs2_a.lastIndexOf(gs2_b)
                            // pick the last btn if there is more than one
                            gs2[gs2_c].elm.click();
                            clicked = true;
                            consoleLog('original fullscreen')
                            break;
                        }
                    }
                    pPlayer = qPlayer
                    qPlayer = qPlayer.parentNode
                }
                if (!clicked) {
                    //cannot find -> default
                    consoleLog('try HTML5 fullscreen')
                    this.setWebFullScreen();
                }
            }
        },
        /* 設置播放速度 */
        setPlaybackRate: function(num, flagTips) {
            let player = $hs.player()
            let curPlaybackRate
            if (num) {
                num = +num
                if (num > 0) { // also checking the type of variable
                    curPlaybackRate = num < 0.1 ? 0.1 : +(num.toFixed(1))
                } else {
                    console.error('h5player: 播放速度轉換出錯')
                    return false
                }
            } else {
                curPlaybackRate = $hs.getPlaybackRate()
            }
            /* 記錄播放速度的信息 */

            let changed = curPlaybackRate !== player.playbackRate;

            if (curPlaybackRate !== player.playbackRate) {

                Store.save('_playback_rate_', curPlaybackRate + '')
                $hs.playbackRate = curPlaybackRate
                player.playbackRate = curPlaybackRate
                /* 本身處於1被播放速度的時候不再提示 */
                //if (!num && curPlaybackRate === 1) return;

            }

            flagTips = (flagTips < 0) ? false : (flagTips > 0) ? true : changed;
            if (flagTips) $hs.tips('Playback speed: ' + player.playbackRate + 'x')
        },
        tuneCurrentTime: function(amount, flagTips) {
            let _amount = +(+amount).toFixed(1);
            let player = $hs.player();
            if (_amount >= 0 || _amount < 0) {} else {
                return;
            }

            let newCurrentTime = player.currentTime + _amount;
            if (newCurrentTime < 0) newCurrentTime = 0;
            if (newCurrentTime > player.duration) newCurrentTime = player.duration;

            let changed = newCurrentTime != player.currentTime && newCurrentTime >= 0 && newCurrentTime <= player.duration;

            if (changed) {
                //player.currentTime = newCurrentTime;
                player.pause();
                let t_ch = (Math.random() / 5 + .75);
                player.currentTime = newCurrentTime * t_ch + player.currentTime * (1.0 - t_ch);
                setTimeout(() => {
                    player.play();
                    player.currentTime = newCurrentTime;
                }, 33);
                flagTips = (flagTips < 0) ? false : (flagTips > 0) ? true : changed;
                $hs.tips(false);
                if (flagTips) {
                    if (_amount > 0) $hs.tips(_amount + ' Sec. Forward', undefined, 3000);
                    else $hs.tips(-_amount + ' Sec. Backward', undefined, 3000)
                }
            }

        },
        tuneVolume: function(amount) {
            let _amount = +(+amount).toFixed(2);

            let player = $hs.player()

            let newVol = player.volume + _amount;
            if (newVol < 0) newVol = 0;
            if (newVol > 1) newVol = 1;
            let chVol = player.volume !== newVol && newVol >= 0 && newVol <= 1;

            if (chVol) {

                if (_amount > 0 && player.volume < 1) {
                    player.volume = newVol // positive
                } else if (_amount < 0 && player.volume > 0) {
                    player.volume = newVol // negative
                }
                $hs.tips(false);
                $hs.tips('Volume: ' + dround(player.volume * 100) + '%', undefined)
            }
        },
        switchPlayStatus: function() {
            let player = $hs.player()
            if (player.paused) {
                player.play()
                if (player._isThisPausedBefore_) {
                    $hs.tips(false);
                    $hs.tips('Playback resumed', undefined, 2500)
                }
            } else {
                player.pause()
                $hs.tips(false);
                $hs.tips('Playback paused', undefined, 2500)
            }
        },
        tipsClassName: 'html_player_enhance_tips',
        tips: function(str, duration, order) {
            let player = $hs.player()
            if (!player) {
                consoleLog('h5Player Tips:', str)
                return true
            }

            if (!player.getAttribute('_h5player_tips')) $hs.initTips();

            let tipsSelector = '#' + (player.getAttribute('_h5player_tips') || $hs.tipsClassName) //if this attribute still doesnt exist, set it to the base cls name
            let tipsDom = getRoot(player).querySelector(tipsSelector)
            if (!tipsDom) {
                consoleLog('init h5player tips dom error...')
                return false
            }
            $hs.change_layoutBox(tipsDom);

            if (str === false) {
                tipsDom.innerText = '';
                tipsDom.setAttribute('_potTips_', '0')
            } else {
                order = order || 1000
                tipsDom.tipsOrder = tipsDom.tipsOrder || 0;

                let shallDisplay = true
                if (order < tipsDom.tipsOrder && getComputedStyle(tipsDom).opacity > 0) shallDisplay = false

                if (shallDisplay) {

                    tipsDom._layoutBoxObj = $hs.getLayoutBox(player)
                    tipsDom._env_changed = 0;

                    if (!_endlessloop) {
                        _endlessloop = new EndlessLoop();
                    }
                    tipsDom._layoutBoxObjSize = null;
                    if (!$hs.__updateTips) {

                        $hs.__updateTips = _endlessloop.append(function(opts) {

                            let tipsDoms = $hs._cached_tipsDoms;

                            if (!tipsDoms || !tipsDoms.length) return;
                            for (const tipsDom of tipsDoms) {

                                if (!tipsDom._layoutBoxObj) return;
                                let {
                                    layoutBox,
                                    wPlayer
                                } = tipsDom._layoutBoxObj
                                if (layoutBox == null || wPlayer == null) return;
                                let p = [~~layoutBox.offsetWidth, ~~layoutBox.offsetHeight, ~~wPlayer.offsetWidth, ~~wPlayer.offsetHeight];

                                if (tipsDom._layoutBoxObjSize) {
                                    let changed = false;
                                    for (let i = 0; i < 4; i++) {
                                        if (tipsDom._layoutBoxObjSize[i] != p[i]) {
                                            changed = true;
                                            break;
                                        }
                                    }
                                    if (!changed) return;
                                    tipsDom._env_changed = (tipsDom._env_changed || 0) + 1;
                                    consoleLog('tipsDom Env changed', tipsDom.id)
                                }

                                tipsDom._layoutBoxObjSize = p;

                                if (!tipsDom.id) return;
                                let player = layoutBox.querySelector(`[_h5player_tips="${tipsDom.id}"]`)
                                if (!player) return;

                                $hs.fixNonBoxingVideoTipsPosition(tipsDom, player)

                            }

                        })

                        $hs.__updateTips.loopingStart();
                    }

                    if (duration === undefined) duration = 2000
                    tipsDom.innerText = str

                    tipsDom.setAttribute('_potTips_', '2')

                    $hs.fixNonBoxingVideoTipsPosition(tipsDom, player);

                    if (duration > 0) {

                        $ws.requestAnimationFrame(() => tipsDom.setAttribute('_potTips_', '1'))

                    } else {
                        order = -1;
                    }

                    tipsDom.tipsOrder = order

                }

            }

            $hs._cached_tipsDoms = document.querySelectorAll('[_potTips_="1"],[_potTips_="2"]'); //48673
        },
        initTips: function() {
            /* 設置提示DOM的樣式 */
            let player = $hs.player()
            let shadowRoot = getRoot(player);
            let doc = player.ownerDocument;
            //console.log((document.documentElement.qq=player),shadowRoot,'xax')
            let parentNode = player.parentNode
            let tcn = player.getAttribute('_h5player_tips') || ($hs.tipsClassName + '_' + (+new Date));
            player.setAttribute('_h5player_tips', tcn)
            if (shadowRoot.querySelector('#' + tcn)) return false;

            if (!shadowRoot._onceAddedCSS) {
                shadowRoot._onceAddedCSS = true;

                var cssStyle = `
                [_potTips_="1"]{
                animation: 2s linear 0s normal forwards 1 delayHide;
                }
                [_potTips_="0"]{
                opacity:0; transform:translate(-9999px);
                }
                [_potTips_="2"]{
                opacity:.95; transform: translate(0,0);
                }

                @keyframes delayHide{
                0%, 99% { opacity:0.95; transform: translate(0,0); }
                100% { opacity:0; transform:translate(-9999px); }
                }
                ` + `
                [_potTips_]{
                font-weight: bold !important;
                position: absolute !important;
                z-index: 999 !important;
                font-size: ${$hs.fontSize || 16}px !important;
                padding: 0px !important;
                border:none !important;
                background: rgba(0,0,0,0) !important;
                color:#738CE6 !important;
                text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
                top: 50%;
                left: 50%;
                max-width:500px;max-height:50px;
                border-radius:3px;
                font-family: 'microsoft yahei', Verdana, Geneva, sans-serif;
                -webkit-user-select: none;
                -moz-user-select: none;
                -ms-user-select: none;
                user-select: none;
                -webkit-touch-callout: none;
                -webkit-user-select: none;
                -khtml-user-drag: none;
                -khtml-user-select: none;
                -moz-user-select: none;
                -moz-user-select: -moz-none;
                -ms-user-select: none;
                pointer-events: none;
                user-select: none;
                }
                `.replace(/\r\n/g, '');

                var cssContainer = (shadowRoot.querySelector('head') || shadowRoot.querySelector('html') || document.documentElement);

                domTool.addStyle(cssStyle, cssContainer);

            }

            let tipsDom = doc.createElement('div')

            var transitionEvent = whichTransitionEvent('animation');

            tipsDom.addEventListener(transitionEvent, function(e) {
                if (this.getAttribute('_potTips_') == '1') {
                    this.setAttribute('_potTips_', '0')
                    $hs._cached_tipsDoms = shadowRoot.querySelectorAll('[_potTips_="1"],[_potTips_="2"]');
                }
            })

            tipsDom.id = tcn;
            tipsDom.setAttribute('unselectable', 'on');
            tipsDom.setAttribute('_potTips_', '0');
            $hs.change_layoutBox(tipsDom);

            tipsDom._vpr = function() {

                let resizeDOM = this;
                if (!resizeDOM) return;

                console.log('The container of the video is resized', resizeDOM.odm.id);

            }
            tipsDom._vpr_pn = tipsDom.parentNode

            tipsDom._vpr = null
            tipsDom._vpr_pn = null

            if (tipsDom._vpr && tipsDom._vpr_pn) {

                let rm = new ResizeODM();
                let tmp_elm = null;
                let {
                    layoutBox,
                    wPlayer
                } = $hs.getLayoutBox(player, false);
                if (layoutBox && wPlayer) tmp_elm = layoutBox.parentNode != tipsDom._vpr_pn ? null : layoutBox;

                rm.relativeParent(tipsDom._vpr_pn, tmp_elm)
                rm.listen(tipsDom._vpr)

                if (rm.odm) {
                    rm.odm.id = tcn + 'obj'
                    //console.log(rm.odm,rm.odm.id)
                    tipsDom._vpr_obj = rm.odm;
                }

            }


            return true;
        },

        responsiveSizing: function(container, elm) {

            let gcssP = getComputedStyle(container);

            let gcssE = getComputedStyle(elm);

            //console.log(gcssE.left,gcssP.width)
            let elmBound = {
                left: parseFloat(gcssE.left) / parseFloat(gcssP.width),
                width: parseFloat(gcssE.width) / parseFloat(gcssP.width),
                top: parseFloat(gcssE.top) / parseFloat(gcssP.height),
                height: parseFloat(gcssE.height) / parseFloat(gcssP.height)
            };

            let elm00 = [elmBound.left, elmBound.top];
            let elm01 = [elmBound.left + elmBound.width, elmBound.top];
            let elm10 = [elmBound.left, elmBound.top + elmBound.height];
            let elm11 = [elmBound.left + elmBound.width, elmBound.top + elmBound.height];

            return {
                elm00,
                elm01,
                elm10,
                elm11,
                plw: elmBound.width,
                plh: elmBound.height
            };

        },

        fixNonBoxingVideoTipsPosition: function(tipsDom, player) {

            if (!tipsDom || !player) return;

            let ct = $hs.getPlayerCTBox(tipsDom, player)

            if (!ct) return;

            //console.log('fixNonBoxingVideoTipsPosition')

            //relative

            let {
                elm00,
                elm01,
                elm10,
                elm11,
                plw,
                plh
            } = $hs.responsiveSizing(ct, player)

            if (isNaN(elm00[0]) || isNaN(elm00[1])) {

                [tipsDom.style.left, tipsDom.style.top] = [player.style.left, player.style.top];
                //eg auto
            } else {

                let rlm00 = elm00.map(t => (t * 100).toFixed(2) + '%');
                //console.log(rlm00);

                [tipsDom.style.left, tipsDom.style.top] = rlm00;

            }

            // absolute

            let _offset = {
                left: 10,
                top: 15
            };

            let customOffset = {
                left: _offset.left,
                top: _offset.top
            };
            let p = tipsDom.getBoundingClientRect();
            let q = player.getBoundingClientRect();
            let currentPos = [p.left, p.top];

            let targetPos = [q.left + player.offsetWidth * 0 + customOffset.left, q.top + player.offsetHeight * 0 + customOffset.top];

            let mL = +tipsDom.style.marginLeft.replace('px', '') || 0;
            if (isNaN(mL)) mL = 0;
            let mT = +tipsDom.style.marginTop.replace('px', '') || 0;
            if (isNaN(mT)) mT = 0;

            let z1 = -(currentPos[0] - targetPos[0]);
            let z2 = -(currentPos[1] - targetPos[1]);

            if (z1 || z2) {

                let y1 = z1 + mL;
                let y2 = z2 + mT;

                tipsDom.style.marginLeft = y1 + 'px';
                tipsDom.style.marginTop = y2 + 'px';

            }
        },

        rotate: 0,
        fps: 30,
        /* 濾鏡效果 */
        filter: {
            key: {},
            view_units: {
                'hue-rotate': 'deg',
                'blur': 'px'
            },
            setup: function(options) {

                var ums = GM_getValue("unsharpen_mask")
                if (!ums) ums = ""

                let view = ''
                let playerElm = $hs.player();
                if (!playerElm) return;
                for (let view_key in this.key) {
                    let view_unit = this.view_units[view_key] || ''
                    view += view_key + '(' + (+this.key[view_key] || 0).toFixed(3) + view_unit + ') '
                    this.key[view_key] = Number(+this.key[view_key] || 0)
                }
                if (ums) view += 'url("#_h5p_' + ums + '")';
                if (options && options.grey) view += ' url("#grey1")';
                playerElm.style.filter = view; //performance in firefox is bad
            },
            reset: function() {
                this.key['brightness'] = 1
                this.key['contrast'] = 1
                this.key['saturate'] = 1
                this.key['hue-rotate'] = 0
                this.key['blur'] = 0
                this.setup()
            }
        },
        //  _isFoucs: false,
        keyMap: _keyMap(["1", "2", "3", "4", "c", "d", "e", "f", "i", "j", "k", "n", "m", "o", "p", "q", "r", "s", "t", "u", "w", "x", "y", "z"], {
            'enter': 13,
            'shift': 16,
            'ctrl': 17,
            'alt': 18,
            'esc': 27,
            'space': 32,
            'LEFT': 37,
            'UP': 38,
            'RIGHT': 39,
            'DOWN': 40,
            'pad1': 97,
            'pad2': 98,
            'pad3': 99,
            'pad4': 100,
            '\\': 220,
        }),
        zoom_keys: ['x', 'c', 'z', 'arrowright', 'arrowleft', 'arrowup', 'arrowdown'],
        /* 播放器事件響應器 */
        playerTrigger: function(player, event) {
            if (!player || !event) return
            let keyCode = lowerKeyCode(event.keyCode)
            if (event.code == "Space" && keyCode > 128) keyCode = 32;
            let keyAsm = (event.shiftKey ? SHIFT : 0) | (event.ctrlKey ? CTRL : 0) | (event.altKey ? ALT : 0);
            //shift + key
            if (keyAsm == SHIFT) {
                let key = event.key.toLowerCase()
                // 網頁FULLSCREEN
                if (key === 'enter') {
                    $hs.callFullScreenBtn()
                    return TERMINATE
                } else if (key == 'f') {
                    //change unsharpen filter

                    var resList = ["unsharpen3_05", "unsharpen3_10", "unsharpen5_05", "unsharpen5_10", "unsharpen9_05", "unsharpen9_10"]
                    var res = (prompt("Enter the unsharpen mask\n(" + resList.map(x => '"' + x + '"').join(', ') + ")", "unsharpen9_05") || "").toLowerCase();
                    if (resList.indexOf(res) < 0) res = ""
                    GM_setValue("unsharpen_mask", res)
                    for (const el of document.querySelectorAll('video[_h5p_uid_encrypted]')) {
                        if (el.style.filter == "" || el.style.filter) {
                            var filterStr1 = el.style.filter.replace(/\s*url\(\"#_h5p_unsharpen[\d\_]+\"\)/, '');
                            var filterStr2 = (res.length > 0 ? ' url("#_h5p_' + res + '")' : '')
                            el.style.filter = filterStr1 + filterStr2;
                        }
                    }
                    return TERMINATE

                }
                // 進入或退出畫中畫模式
                else if (key === 'p') {
                    $hs.pictureInPicture(player)

                    return TERMINATE
                } else if (key == 'r') {
                    if (player._h5player_lastrecord_ !== null && (player._h5player_lastrecord_ >= 0 || player._h5player_lastrecord_ <= 0)) {
                        $hs.setPlayProgress(player, player._h5player_lastrecord_)

                        return TERMINATE
                    }

                } else if (key === 'o') {
                    var _debug_h5p_logging_ch = false;
                    try {
                        window.localStorage.setItem('_h5_player_sLogging_', 1 - window.localStorage.getItem('_h5_player_sLogging_'))
                        _debug_h5p_logging_ = +window.localStorage.getItem('_h5_player_sLogging_') > 0;
                        _debug_h5p_logging_ch = true;
                    } catch (e) {

                    }
                    consoleLogF('_debug_h5p_logging_', !!_debug_h5p_logging_, 'changed', _debug_h5p_logging_ch)

                    if (_debug_h5p_logging_ch) {

                        return TERMINATE
                    }
                } else if (key == 't') {
                    if (/^blob/i.test(player.currentSrc)) {
                        alert(`The current video is ${player.currentSrc}\nSorry, it cannot be opened in PotPlayer.`);
                    } else {
                        var confirm_res = confirm(`The current video is ${player.currentSrc}\nDo you want to open it in PotPlayer?`);
                        if (confirm_res) window.open('potplayer://' + player.currentSrc, '_blank');
                    }
                    return TERMINATE
                }
                // 視頻畫面縮放相關事件

                let videoScale = ($hs.lastVideoScale>0)?$hs.lastVideoScale: 1.0;
                let key_controlled = true;
                switch (key) {
                    // shift+X:視頻縮小 -0.1
                    case 'x':
                        videoScale -= 0.1
                        if(videoScale<0.1) videoScale=0.1;
                        break
                        // shift+C:視頻放大 +0.1
                    case 'c':
                        videoScale += 0.1
                        if(videoScale>16) videoScale=16;
                        break
                        // shift+Z:視頻恢復正常大小
                    case 'z':
                        videoScale = 1.0
                        $hs.translate = {
                            x: 0,
                            y: 0
                        }
                        break
                    case 'arrowright':
                        $hs.translate.x += 10
                        break
                    case 'arrowleft':
                        $hs.translate.x -= 10
                        break
                    case 'arrowup':
                        $hs.translate.y -= 10
                        break
                    case 'arrowdown':
                        $hs.translate.y += 10
                        break
                    default:
                        key_controlled = false;

                }
                if (key_controlled) {

                    videoScale = $hs.lastVideoScale = +videoScale.toFixed(1);
                    player.style.transform = `scale(${videoScale}) translate(${$hs.translate.x}px, ${$hs.translate.y}px)`
                    let tipsMsg = `視頻縮放率:${ +(videoScale * 100).toFixed(2)  }%`
                    if ($hs.translate.x) {
                        tipsMsg += `,水平位移:${$hs.translate.x}px`
                    }
                    if ($hs.translate.y) {
                        tipsMsg += `,垂直位移:${$hs.translate.y}px`
                    }
                    $hs.tips(false);
                    $hs.tips(tipsMsg)

                    return TERMINATE
                }

            }
            // 防止其它無關組合鍵衝突
            if (!keyAsm) {
                let kControl = null
                let newPBR, oldPBR;
                switch (keyCode) {
                    // 方向鍵右→:快進3秒
                    case 39:
                        $hs.tuneCurrentTime($hs.skipStep);
                        return TERMINATE;
                        break;
                        // 方向鍵左←:後退3秒
                    case 37:
                        $hs.tuneCurrentTime(-$hs.skipStep);
                        return TERMINATE;
                        break;
                        // 方向鍵上↑:音量升高 1%
                    case 38:
                        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.01);
                        return TERMINATE;
                        break;
                        // 方向鍵下↓:音量降低 1%
                    case 40:

                        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.01);
                        return TERMINATE;
                        break;
                        // 空格鍵:暫停/播放
                    case $hs.keyMap.space:
                        $hs.switchPlayStatus();
                        return TERMINATE;
                        break;
                        // 按鍵X:減速播放 -0.1
                    case $hs.keyMap.x:
                        if (player.playbackRate > 0) {
                            $hs.tips(false);
                            $hs.setPlaybackRate(player.playbackRate - 0.1);
                            return TERMINATE
                        }
                        break;
                        // 按鍵C:加速播放 +0.1
                    case $hs.keyMap.c:
                        if (player.playbackRate < 16) {
                            $hs.tips(false);
                            $hs.setPlaybackRate(player.playbackRate + 0.1);
                            return TERMINATE
                        }

                        break;
                        // 按鍵Z:正常速度播放
                    case $hs.keyMap.z:
                        $hs.tips(false);
                        oldPBR = player.playbackRate;
                        if (oldPBR != 1.0) {
                            player._playbackRate_z = oldPBR;
                            newPBR = 1.0;
                        } else if (player._playbackRate_z != 1.0) {
                            newPBR = player._playbackRate_z || 1.0;
                            player._playbackRate_z = 1.0;
                        } else {
                            newPBR = 1.0
                            player._playbackRate_z = 1.0;
                        }
                        $hs.setPlaybackRate(newPBR, 1)
                        return TERMINATE
                        break;
                        // 按鍵F:下一幀
                    case $hs.keyMap.f:
                        if (window.location.hostname === 'www.netflix.com') return /* netflix 的F鍵是FULLSCREEN的意思 */
                        $hs.tips(false);
                        if (!player.paused) player.pause()
                        player.currentTime += +(1 / $hs.fps)
                        $hs.tips('Jump to: Next frame')
                        return TERMINATE
                        break;
                        // 按鍵D:上一幀
                    case $hs.keyMap.d:
                        $hs.tips(false);
                        if (!player.paused) player.pause()
                        player.currentTime -= +(1 / $hs.fps)
                        $hs.tips('Jump to: Previous frame')
                        return TERMINATE
                        break;
                        // 按鍵E:亮度增加%
                    case $hs.keyMap.e:
                        $hs.tips(false);
                        kControl = 'brightness'
                        $hs.filter.key[kControl] += 0.1
                        $hs.filter.key[kControl] = $hs.filter.key[kControl].toFixed(2)
                        $hs.filter.setup()
                        $hs.tips('Brightness: ' + dround($hs.filter.key[kControl] * 100) + '%')
                        return TERMINATE
                        break;
                        // 按鍵W:亮度減少%
                    case $hs.keyMap.w:
                        $hs.tips(false);
                        kControl = 'brightness'
                        if ($hs.filter.key[kControl] > 0) {
                            $hs.filter.key[kControl] -= 0.1
                            $hs.filter.key[kControl] = $hs.filter.key[kControl].toFixed(2)
                            $hs.filter.setup()
                        }
                        $hs.tips('Brightness: ' + dround($hs.filter.key[kControl] * 100) + '%')
                        return TERMINATE
                        break;
                        // 按鍵T:對比度增加%
                    case $hs.keyMap.t:
                        $hs.tips(false);
                        kControl = 'contrast'
                        $hs.filter.key[kControl] += 0.1
                        $hs.filter.key[kControl] = $hs.filter.key[kControl].toFixed(2)
                        $hs.filter.setup()
                        $hs.tips('Contrast: ' + dround($hs.filter.key[kControl] * 100) + '%')
                        return TERMINATE
                        break;
                        // 按鍵R:對比度減少%
                    case $hs.keyMap.r:
                        $hs.tips(false);
                        kControl = 'contrast'
                        if ($hs.filter.key[kControl] > 0) {
                            $hs.filter.key[kControl] -= 0.1
                            $hs.filter.key[kControl] = $hs.filter.key[kControl].toFixed(2)
                            $hs.filter.setup()
                        }
                        $hs.tips('Contrast: ' + dround($hs.filter.key[kControl] * 100) + '%')
                        return TERMINATE
                        break;
                        // 按鍵U:飽和度增加%
                    case $hs.keyMap.u:
                        $hs.tips(false);
                        kControl = 'saturate'
                        $hs.filter.key[kControl] += 0.1
                        $hs.filter.key[kControl] = $hs.filter.key[kControl].toFixed(2)
                        $hs.filter.setup()
                        $hs.tips('Saturate: ' + dround($hs.filter.key[kControl] * 100) + '%')
                        return TERMINATE
                        break;
                        // 按鍵Y:飽和度減少%
                    case $hs.keyMap.y:
                        $hs.tips(false);
                        kControl = 'saturate'
                        if ($hs.filter.key[kControl] > 0) {
                            $hs.filter.key[kControl] -= 0.1
                            $hs.filter.key[kControl] = $hs.filter.key[kControl].toFixed(2)
                            $hs.filter.setup()
                        }
                        $hs.tips('Saturate: ' + dround($hs.filter.key[kControl] * 100) + '%')
                        return TERMINATE
                        break;
                        // 按鍵O:色相增加 1 度
                    case $hs.keyMap.o:
                        $hs.tips(false);
                        kControl = 'hue-rotate'
                        $hs.filter.key['hue-rotate'] += 1
                        $hs.filter.setup()
                        $hs.tips('Hue: ' + $hs.filter.key[kControl] + ' deg')
                        return TERMINATE
                        break;
                        // 按鍵I:色相減少 1 度
                    case $hs.keyMap.i:
                        $hs.tips(false);
                        kControl = 'hue-rotate'
                        $hs.filter.key['hue-rotate'] -= 1
                        $hs.filter.setup()
                        $hs.tips('Hue: ' + $hs.filter.key[kControl] + ' deg')
                        return TERMINATE
                        break;
                        // 按鍵K:模糊增加 0.1 px
                    case $hs.keyMap.k:
                        $hs.tips(false);
                        kControl = 'blur'
                        $hs.filter.key[kControl] += 0.1
                        $hs.filter.key[kControl] = (+$hs.filter.key[kControl] || 0).toFixed(1)
                        $hs.filter.setup()
                        $hs.tips('Blur: ' + $hs.filter.key[kControl] + ' px')
                        return TERMINATE
                        break;
                        // 按鍵J:模糊減少 0.1 px
                    case $hs.keyMap.j:
                        $hs.tips(false);
                        kControl = 'blur'
                        if ($hs.filter.key[kControl] > 0) {
                            $hs.filter.key[kControl] -= 0.1
                            $hs.filter.key[kControl] = (+$hs.filter.key[kControl] || 0).toFixed(1)
                            $hs.filter.setup()
                        }
                        $hs.tips('Blur: ' + $hs.filter.key[kControl] + ' px')
                        return TERMINATE
                        break;
                        // 按鍵Q:圖像復位
                    case $hs.keyMap.q:
                        $hs.tips(false);
                        $hs.filter.reset()
                        $hs.tips('Video Filter Reset')
                        return TERMINATE
                        break;
                        // 按鍵S:畫面旋轉 90 度
                    case $hs.keyMap.s:
                        $hs.tips(false);
                        $hs.rotate += 90
                        if ($hs.rotate % 360 === 0) $hs.rotate = 0;
                        player.style.transform = 'rotate(' + $hs.rotate + 'deg)'
                        $hs.tips('Rotation:' + $hs.rotate + ' deg')
                        return TERMINATE
                        break;
                        // 按鍵迴車,進入FULLSCREEN
                    case $hs.keyMap.enter:
                        //t.callFullScreenBtn();
                        break;
                    case $hs.keyMap.n:
                        $hs.pictureInPicture(player);
                        return TERMINATE
                        break;
                    case $hs.keyMap.m:
                        //console.log('m!', player.volume,player._volume)

                        if (player.volume >= 0) {

                            if (!player.volume || player.muted) {

                                var newVol = player.volume || player._volume || 0.5;
                                if (player.volume !== newVol) {
                                    player.volume = newVol;
                                }
                                player.muted = false;
                                $hs.tips(false);
                                $hs.tips('Mute: Off', undefined);

                            } else {

                                player._volume = player.volume;
                                player._volume_p = player.volume;
                                //player.volume = 0;
                                player.muted = true;
                                $hs.tips(false);
                                $hs.tips('Mute: On', undefined);

                            }

                        }

                        return TERMINATE
                        break;
                    default:
                        // 按1-4設置播放速度 49-52;97-100
                        if ((keyCode >= 49 && keyCode <= 52) || (keyCode >= 97 && keyCode <= 100)) {
                            $hs.tips(false);
                            $hs.setPlaybackRate(Number(event.key), 1)
                            return TERMINATE
                        }
                }

            }
        },

        handlerPlayerMouseMove: function(e) {
            let player = $hs.player();
            let rootNode = getRoot(player);

            if (rootNode.pointerLockElement != player) {
                player.removeEventListener('mousemove', $hs.handlerPlayerMouseMove)
                return;
            }

            var movementX = e.movementX || e.mozMovementX || e.webkitMovementX || 0,
                movementY = e.movementY || e.mozMovementY || e.webkitMovementY || 0;

            player.__xyOffset.x += movementX
            player.__xyOffset.y += movementY
            var ld = Math.sqrt(screen.width * screen.width + screen.height * screen.height) * .1
            var md = Math.sqrt(player.__xyOffset.x * player.__xyOffset.x + player.__xyOffset.y * player.__xyOffset.y);
            if (md > ld) $hs.playerActionLeave();

        },

        playerActionEnter: function() {
            let player = $hs.player();

            if (player) {
                player.__requestPointerLock();
                player.__xyOffset = {
                    x: 0,
                    y: 0
                };
                player.addEventListener('mousemove', $hs.handlerPlayerMouseMove)
            }
        },

        playerActionLeave: function() {
            let player = $hs.player();
            if (player) player.removeEventListener('mousemove', $hs.handlerPlayerMouseMove)
            document.__exitPointerLock();
        },

        /* 按鍵響應方法 */
        handlerRootKeyDownEvent: function(event) {
            if ($hs.intVideoInitCount > 0) {} else {
                return;
            }

            let keyCode = lowerKeyCode(event.keyCode)
            let key = event.key.toLowerCase()
            let player = $hs.player()

            if (!player) return; // no video tag

            let rootNode = getRoot(player);

            let keyAsm = (event.shiftKey ? SHIFT : 0) | (event.ctrlKey ? CTRL : 0) | (event.altKey ? ALT : 0);

            if (!keyAsm && keyCode == 27 && (document.fullscreenElement || rootNode.pointerLockElement)) {
                setTimeout(() => {
                    if (document.fullscreenElement) {
                        document.exitFullscreen();
                    } else if (document.pointerLockElement) {
                        $hs.playerActionLeave();
                    }
                }, 700);
                return;
            }

            if (event.code == "Space" && keyCode > 128) keyCode = 32;
            if (isInOperation(event.target)) return;

            //console.log('K01')

            /* 切換插件的可用狀態 */
            // Shift-`
            if (keyAsm == SHIFT && keyCode === 192) {
                $hs.enable = !$hs.enable;
                $hs.tips(false);
                if ($hs.enable) {
                    $hs.tips('啟用h5Player插件')
                } else {
                    $hs.tips('禁用h5Player插件')
                }
                // 阻止事件冒泡
                event.stopPropagation()
                event.preventDefault()
                return false
            }
            if (!$hs.enable) {
                consoleLog('h5Player 已禁用~')
                return false
            }

            /* 非全局模式下,不聚焦則不執行快捷鍵的操作 */

            if (!keyAsm && key == 'enter' && !isInOperation()) {
                if (!rootNode.pointerLockElement && !document.fullscreenElement) {
                    if (rootNode.pointerLockElement != player) {
                        $hs.playerActionEnter();

                        // 阻止事件冒泡
                        event.stopPropagation()
                        event.preventDefault()
                        return false
                    }
                } else if (rootNode.pointerLockElement && !document.fullscreenElement) {
                    $hs.playerActionLeave();

                    // 阻止事件冒泡
                    event.stopPropagation()
                    event.preventDefault()
                    return false
                } else if (document.fullscreenElement) {
                    document.exitFullscreen();

                    // 阻止事件冒泡
                    event.stopPropagation()
                    event.preventDefault()
                    return false
                }
            }

            let hv = (elm) => (elm && (elm == player || elm.contains(player)) ? elm : null);

            let _checkingPass;

            let plm = null;
            if (rootNode.activeElement && !rootNode.pointerLockElement && !document.fullscreenElement) {
                // the active element may or may not contains the player
                // but the player box (player->parent->parent->...) shall contains the active element (and the player)
                // so if the active element is inside the layoutbox, okay!
                // ps. layoutbox may be much larger than the activeelement, then it is not overlapping case.
                plm = rootNode.activeElement
                _checkingPass = $hs.isInActiveMode(rootNode.activeElement, player);
            } else {
                plm = hv(rootNode.pointerLockElement) || hv(document.fullscreenElement)
                _checkingPass = !!plm
            }

            if (_checkingPass) {

                if (isInOperation()) return

                $hs.keyCodeList = $hs.keyCodeList || Object.values($hs.keyMap);
                $hs.keyList = $hs.keyList || (Object.keys($hs.keyMap).concat(['enter', 'shift', 'control', 'alt', 'escape', ' ', 'space', 'arrowleft', 'arrowright', 'arrowright', 'arrowup', 'arrowdown', '|']));
                let isInUseCode = $hs.keyCodeList.includes(keyCode) || $hs.keyList.includes(key);
                if (!isInUseCode) return

                let res = $hs.playerTrigger(player, event)
                if (res == TERMINATE) {
                    event.stopPropagation()
                    event.preventDefault()
                    return false
                }

            }
        },
        /* 設置播放進度 */
        setPlayProgress: function(player, curTime) {
            if (!player) return
            if (!curTime || Number.isNaN(curTime)) return
            player.currentTime = curTime
            if (curTime > 3) {
                $hs.tips(false);
                $hs.tips(`Playback Jumps to ${$hs.toolFormatCT(curTime)}`)
                if (player.paused) player.play();
            }
        }
    }

    function makeFilter(arr, k) {
        var res = ""
        for (const e of arr) {
            for (const d of e) {
                res += " " + (1.0 * d * k).toFixed(9)
            }
        }
        return res.trim()
    }

    function _add_filter(rootElm) {
        var rootView = null;
        if (rootElm && rootElm.nodeType > 0) {
            while (rootElm.parentNode && rootElm.parentNode.nodeType === 1) rootElm = rootElm.parentNode;
            rootView = rootElm.querySelector('body') || rootElm;
        } else {
            return;
        }

        if (rootView && rootView.querySelector && !rootView.querySelector('#_h5player_section_')) {

            let svgFilterElm = document.createElement('section')
            svgFilterElm.style.position = 'fixed';
            svgFilterElm.style.left = '-999px';
            svgFilterElm.style.width = '1px';
            svgFilterElm.style.top = '-999px';
            svgFilterElm.style.height = '1px';
            svgFilterElm.id = '_h5player_section_'
            var svgXML = `
                <svg id='_h5p_image' version="1.1" xmlns="http://www.w3.org/2000/svg">
                <defs>
                <filter id="_h5p_sharpen1">
                <feConvolveMatrix filterRes="100 100" style="color-interpolation-filters:sRGB" order="3" kernelMatrix="` + `
                -0.3 -0.3 -0.3
                -0.3 3.4 -0.3
                -0.3 -0.3 -0.3`.replace(/[\n\r]+/g, '  ').trim() + `"  preserveAlpha="true"/>
                </filter>
                <filter id="_h5p_unsharpen1">
                <feConvolveMatrix style="color-interpolation-filters:sRGB;color-interpolation: sRGB;" order="5" kernelMatrix="` +
                makeFilter([
                    [1, 4, 6, 4, 1],
                    [4, 16, 24, 16, 4],
                    [6, 24, -476, 24, 6],
                    [4, 16, 24, 16, 4],
                    [1, 4, 6, 4, 1]
                ], -1 / 256) + `"  preserveAlpha="false"/>
                </filter>
                <filter id="_h5p_unsharpen3_05">
                <feConvolveMatrix style="color-interpolation-filters:sRGB;color-interpolation: sRGB;" order="3" kernelMatrix="` +
                makeFilter(
                    [
                        [0.025, 0.05, 0.025],
                        [0.05, -1.1, 0.05],
                        [0.025, 0.05, 0.025]
                    ], -1 / .8) + `"  preserveAlpha="false"/>
                </filter>
                <filter id="_h5p_unsharpen3_10">
                <feConvolveMatrix style="color-interpolation-filters:sRGB;color-interpolation: sRGB;" order="3" kernelMatrix="` +
                makeFilter(
                    [
                        [0.05, 0.1, 0.05],
                        [0.1, -1.4, 0.1],
                        [0.05, 0.1, 0.05]
                    ], -1 / .8) + `"  preserveAlpha="false"/>
                </filter>
                <filter id="_h5p_unsharpen5_05">
                <feConvolveMatrix style="color-interpolation-filters:sRGB;color-interpolation: sRGB;" order="5" kernelMatrix="` +
                makeFilter(
                    [
                        [0.025, 0.1, 0.15, 0.1, 0.025],
                        [0.1, 0.4, 0.6, 0.4, 0.1],
                        [0.15, 0.6, -18.3, 0.6, 0.15],
                        [0.1, 0.4, 0.6, 0.4, 0.1],
                        [0.025, 0.1, 0.15, 0.1, 0.025]
                    ], -1 / 12.8) + `"  preserveAlpha="false"/>
                </filter>
                <filter id="_h5p_unsharpen5_10">
                <feConvolveMatrix style="color-interpolation-filters:sRGB;color-interpolation: sRGB;" order="5" kernelMatrix="` +
                makeFilter(
                    [
                        [0.05, 0.2, 0.3, 0.2, 0.05],
                        [0.2, 0.8, 1.2, 0.8, 0.2],
                        [0.3, 1.2, -23.8, 1.2, 0.3],
                        [0.2, 0.8, 1.2, 0.8, 0.2],
                        [0.05, 0.2, 0.3, 0.2, 0.05]
                    ], -1 / 12.8) + `"  preserveAlpha="false"/>
                </filter>
                <filter id="_h5p_unsharpen9_05">
                <feConvolveMatrix style="color-interpolation-filters:sRGB;color-interpolation: sRGB;" order="9" kernelMatrix="` +
                makeFilter(
                    [
                        [0.025, 0.2, 0.7, 1.4, 1.75, 1.4, 0.7, 0.2, 0.025],
                        [0.2, 1.6, 5.6, 11.2, 14, 11.2, 5.6, 1.6, 0.2],
                        [0.7, 5.6, 19.6, 39.2, 49, 39.2, 19.6, 5.6, 0.7],
                        [1.4, 11.2, 39.2, 78.4, 98, 78.4, 39.2, 11.2, 1.4],
                        [1.75, 14, 49, 98, -4792.7, 98, 49, 14, 1.75],
                        [1.4, 11.2, 39.2, 78.4, 98, 78.4, 39.2, 11.2, 1.4],
                        [0.7, 5.6, 19.6, 39.2, 49, 39.2, 19.6, 5.6, 0.7],
                        [0.2, 1.6, 5.6, 11.2, 14, 11.2, 5.6, 1.6, 0.2],
                        [0.025, 0.2, 0.7, 1.4, 1.75, 1.4, 0.7, 0.2, 0.025]
                    ], -1 / 3276.8) + `"  preserveAlpha="false"/>
                </filter>
                <filter id="_h5p_unsharpen9_10">
                <feConvolveMatrix style="color-interpolation-filters:sRGB;color-interpolation: sRGB;" order="9" kernelMatrix="` +
                makeFilter(
                    [
                        [0.05, 0.4, 1.4, 2.8, 3.5, 2.8, 1.4, 0.4, 0.05],
                        [0.4, 3.2, 11.2, 22.4, 28, 22.4, 11.2, 3.2, 0.4],
                        [1.4, 11.2, 39.2, 78.4, 98, 78.4, 39.2, 11.2, 1.4],
                        [2.8, 22.4, 78.4, 156.8, 196, 156.8, 78.4, 22.4, 2.8],
                        [3.5, 28, 98, 196, -6308.6, 196, 98, 28, 3.5],
                        [2.8, 22.4, 78.4, 156.8, 196, 156.8, 78.4, 22.4, 2.8],
                        [1.4, 11.2, 39.2, 78.4, 98, 78.4, 39.2, 11.2, 1.4],
                        [0.4, 3.2, 11.2, 22.4, 28, 22.4, 11.2, 3.2, 0.4],
                        [0.05, 0.4, 1.4, 2.8, 3.5, 2.8, 1.4, 0.4, 0.05]
                    ], -1 / 3276.8) + `"  preserveAlpha="false"/>
                    </filter>
                    <filter id="_h5p_grey1">
                    <feColorMatrix values="0.3333 0.3333 0.3333 0 0
                    0.3333 0.3333 0.3333 0 0
                    0.3333 0.3333 0.3333 0 0
                    0      0      0      1 0"/>
                    <feColorMatrix type="saturate" values="0" />
                    </filter>
                    </defs>
                    </svg>
                    `;

            svgFilterElm.innerHTML = svgXML.replace(/[\r\n\s]+/g, ' ').trim();

            rootView.appendChild(svgFilterElm);
        }

    }

    /**
     * 某些網頁用了attachShadow closed mode,需要open才能獲取video標籤,例如百度雲盤
     * 解決參考:
     * https://developers.google.com/web/fundamentals/web-components/shadowdom?hl=zh-cn#closed
     * https://stackoverflow.com/questions/54954383/override-element-prototype-attachshadow-using-chrome-extension
     */

    const initForShadowRoot = async (shadowRoot) => {
        try {
            if (shadowRoot && shadowRoot.nodeType > 0 && 'querySelectorAll' in shadowRoot) {
                $hs.bindDocEvents(shadowRoot);
                new VideoListener(shadowRoot).listen(handlerVideoFound)
                shadowRoots.push(shadowRoot)
            }
        } catch (e) {
            console.log('h5Player: initForShadowRoot failed')
        }
    }

    function hackAttachShadow() { // attachShadow - DOM Standard

        var _prototype_ = window && window.HTMLElement ? window.HTMLElement.prototype : null;
        if (_prototype_ && typeof _prototype_.attachShadow == 'function') {

            var _attachShadow = _prototype_.attachShadow

            hackAttachShadow = null
            _prototype_.attachShadow = function() {
                let arg = [...arguments];
                if (arg[0] && arg[0].mode) arg[0].mode = 'open';
                let shadowRoot = _attachShadow.apply(this, arg);
                initForShadowRoot(shadowRoot);
                return shadowRoot
            }

        }

    }

    function hackCreateShadowRoot() { // createShadowRoot - Deprecated

        var _prototype_ = window && window.HTMLElement ? window.HTMLElement.prototype : null;
        if (_prototype_ && typeof _prototype_.createShadowRoot == 'function') {

            var _createShadowRoot = _prototype_.createShadowRoot;

            hackCreateShadowRoot = null
            _prototype_.createShadowRoot = function() {
                const shadowRoot = _createShadowRoot.apply(this, arguments);
                initForShadowRoot(shadowRoot);
                return shadowRoot;
            }

        }
    }

    /* 事件偵聽hack */
    function hackEventListener() {
        if (!window.EventTarget) return;
        const eventTargetPrototype = window.EventTarget.prototype;
        var _addEventListener = eventTargetPrototype.addEventListener;
        var _removeEventListener = eventTargetPrototype.removeEventListener;
        if (typeof _addEventListener == 'function' && typeof _removeEventListener == 'function') {} else return;
        hackEventListener = null;

        class Listeners {

            constructor(dom, type) {
                this._dom = dom;
                this._type = type;
                this.listenersCount = 0;
                this.hashList = {};
            }
            get baseFunc() {
                if (this._dom && this._type) {
                    return this._dom['on' + this._type];
                }
            }
            get funcCount() {
                if (this._dom && this._type) {
                    return (typeof this.baseFunc == 'function') * 1 + (this.listenersCount || 0)
                }
            }


        }



        var hackedEvtCount = 0;


        function timeupdateHack(evtTarget, args) {
            consoleLog('timeupdate detected')
            if (!evtTarget._onceTimeUpdateHack) {
                evtTarget._onceTimeUpdateHack = true;
                if (!_endlessloop) _endlessloop = new EndlessLoop();
                let opts = _endlessloop.append(handle.timeupdatef_ell)
                opts.video = evtTarget;
                opts.lastTime = -1;
                opts.evt = new SimEvent("timeupdatef", {
                    bubbles: false,
                    cancelable: true,
                }, 'timeupdate');
                opts.loopingStart();
                _ell_timeupdatefs.push(opts);
            }
            args[0] = 'timeupdatef';
            args[1] = sim_arg_1_fn(args[1], _evtOB_create._timeupdate())
            _addEventListener.apply(evtTarget, args)
            consoleLog('timeupdate(fast) is added with listener', fn_toString(listener))
        }

        eventTargetPrototype.addEventListener = function() {
            if (!this || !(this instanceof EventTarget)) {
                return _addEventListener.apply(this, arguments)
                //unknown bug?
            }

            let arg = arguments
            let type = arg[0]
            let listener = arg[1]
            let boolCapture = !!((arg[2] && typeof arg[2] == 'object') ? arg[2].capture : arg[2])
            let sim_arg = null;
            if (type == 'timeupdate' && this.nodeType == 1 && this.nodeName == "VIDEO" && 0) {
                timeupdateHack(this, [...arg])
            } else {
                _addEventListener.apply(this, arg)
            }

            this._listeners = this._listeners || {}
            this._listeners[type] = this._listeners[type] || new Listeners(this, type)
            let uid = 100000 + (++hackedEvtCount);
            let listenerObj = {
                listener,
                options: arg[2],
                uid: uid,
                useCapture: boolCapture,
                sim_arg
            }
            this._listeners[type].hashList[uid + ''] = listenerObj;
            this._listeners[type].listenersCount++;
        }
        // hack removeEventListener
        eventTargetPrototype.removeEventListener = function() {
            if (!this || !(this instanceof EventTarget)) {
                return _removeEventListener.apply(this, arguments)
                //unknown bug?
            }

            let arg = arguments
            let type = arg[0]
            let listener = arg[1]

            let boolCapture = !!((arg[2] && typeof arg[2] == 'object') ? arg[2].capture : arg[2])

            let defaultRemoval = true;
            if (this._listeners && this._listeners[type] && this._listeners[type].hashList) {
                let hashList = this._listeners[type].hashList
                for (let k in hashList) {
                    if (hashList[k].listener === listener && hashList[k].useCapture == boolCapture) {
                        if (hashList[k].sim_arg) {
                            defaultRemoval = false;
                            _removeEventListener.apply(this, hashList[k].sim_arg.slice(0, 3));
                        }
                        delete hashList[k];
                        this._listeners[type].listenersCount--;
                        break;
                    }
                }
            }
            if (defaultRemoval) _removeEventListener.apply(this, arg);
        }
    }

    function handlerVideoFound(video) {

        if (!video) return;
        if (video.getAttribute('_h5ppid')) return;
        var alabel = video.getAttribute('aria-label')
        if (alabel && typeof alabel == "string" && alabel.toUpperCase() == "GIF") return;
        consoleLog('handlerVideoFound', video)

        $hs.intVideoInitCount = ($hs.intVideoInitCount || 0) + 1;
        consoleLog(' - HTML5 Video is detected -', `Number of Videos: ${$hs.intVideoInitCount}`)
        if ($hs.intVideoInitCount === 1) $hs.fireGlobalInit();
        video.setAttribute('_h5ppid', 'h5p-' + $hs.intVideoInitCount)

        let rootNode = getRoot(video);
        if (rootNode.host) $hs.getLayoutBox(video);
        let rootElm = rootNode.querySelector('head') || rootNode.querySelector('html') || document.documentElement //48763
        _add_filter(rootElm)

        video.addEventListener('loadedmetadata', $hs.handlerVideoLoadedMetaData, true);

    }


    ResizeODM.__init__();

    hackAttachShadow()
    hackCreateShadowRoot()
    hackEventListener()


    window.addEventListener('message', $hs.handlerWinMessage, false);
    $hs.bindDocEvents(document);
    new VideoListener(document.documentElement).listen(handlerVideoFound)

    var windowsLD = (function() {
        let ls_res = [];
        try {
            ls_res = [!!window.localStorage, !!window.top.localStorage];
        } catch (e) {}
        try {
            let winp = window;
            let winc = 0;
            while (winp !== window.top && winp && ++winc) winp = winp.parentNode;
            ls_res.push(winc);
        } catch (e) {}
        return ls_res;
    })();

    consoleLogF('- h5Player Plugin Loaded -', ...windowsLD)

    function isInCrossOriginFrame() {
        let result = true;
        try {
            if (window.top.localStorage || window.top.location.href) result = false;
        } catch (e) {}
        return result
    }

    if (isInCrossOriginFrame()) consoleLog('cross origin frame detected');


})(null);