// ==UserScript== // @name HTML5播放器增强插件 - 修订版 // @namespace https://github.com/xxxily/h5player // @homepage https://github.com/xxxily/h5player // @version 2.3.2 // @description 对HTML5播放器的功能进行增强,支持所有使用H5进行视频播放的网站,快捷键仿照Potplayer的快捷键布局,实现调节亮度,饱和度,对比度,速度等功能。 // @author ankvps // @match http://*/* // @match https://*/* // @run-at document-start // @grant GM_addStyle // @downloadURL none // ==/UserScript== /* 元素全屏API,同时兼容网页全屏 */ class FullScreen { constructor (dom, pageMode) { this.dom = dom // 默认全屏模式,如果传入pageMode则表示进行的是页面全屏操作 this.pageMode = pageMode || false let fullPageStyle = ` ._webfullscreen_ { display: block !important; position: fixed !important; width: 100% !important; height: 100% !important; top: 0 !important; left: 0 !important; background: #000 !important; } ._webfullscreen_zindex_ { z-index: 999998 !important; } ` if (!window._hasInitFullPageStyle_) { GM_addStyle(fullPageStyle) window._hasInitFullPageStyle_ = true } } eachParentNode (dom, fn) { let parent = dom.parentNode while (parent && parent.classList) { let isEnd = fn(parent, dom) parent = parent.parentNode if (isEnd) { break } } } getContainer () { let t = this if (t._container_) return t._container_ let d = t.dom let domBox = d.getBoundingClientRect() let container = d t.eachParentNode(d, function (parentNode) { let parentBox = parentNode.getBoundingClientRect() if (parentBox.width <= domBox.width && parentBox.height <= domBox.height) { container = parentNode } else { return true } }) t._container_ = container return container } isFull () { return this.dom.classList.contains('_webfullscreen_') } isFullScreen () { let d = document return !!(d.fullscreen || d.webkitIsFullScreen || d.mozFullScreen || d.fullscreenElement || d.webkitFullscreenElement || d.mozFullScreenElement) } enterFullScreen () { let c = this.getContainer() let enterFn = c.requestFullscreen || c.webkitRequestFullScreen || c.mozRequestFullScreen || c.msRequestFullScreen enterFn && enterFn.call(c) } enter () { let t = this if (t.isFull()) return let container = t.getContainer() let needSetIndex = false if (t.dom === container) { needSetIndex = true } this.eachParentNode(t.dom, function (parentNode) { parentNode.classList.add('_webfullscreen_') if (container === parentNode || needSetIndex) { needSetIndex = true parentNode.classList.add('_webfullscreen_zindex_') } }) t.dom.classList.add('_webfullscreen_') let fullScreenMode = !t.pageMode if (fullScreenMode) { t.enterFullScreen() } } exitFullScreen () { let d = document let exitFn = d.exitFullscreen || d.webkitExitFullscreen || d.mozCancelFullScreen || d.msExitFullscreen exitFn && exitFn.call(d) } exit () { let t = this t.dom.classList.remove('_webfullscreen_') this.eachParentNode(t.dom, function (parentNode) { parentNode.classList.remove('_webfullscreen_') parentNode.classList.remove('_webfullscreen_zindex_') }) let fullScreenMode = !t.pageMode if (fullScreenMode || t.isFullScreen()) { t.exitFullScreen() } } toggle () { this.isFull() ? this.exit() : this.enter() } } (function () { /** * 任务配置中心 Task Control Center * 用于配置所有无法进行通用处理的任务,如不同网站的全屏方式不一样,必须调用网站本身的全屏逻辑,才能确保字幕、弹幕等正常工作 * */ const TCC = { /** * 配置示例 * 父级键名对应的是一级域名, * 子级键名对应的相关功能名称,键值对应的该功能要触发的点击选择器或者要调用的相关函数 * 所有子级的键值都支持使用选择器触发或函数调用 * 配置了子级的则使用子级配置逻辑进行操作,否则使用默认逻辑 * 注意:include,exclude这两个子级键名除外,这两个是用来进行url范围匹配的 * */ 'demo.demo': { 'fullScreen': '.fullscreen-btn', 'exitFullScreen': '.exit-fullscreen-btn', 'webFullScreen': function () {}, 'exitWebFullScreen': '.exit-fullscreen-btn', 'autoPlay': '.player-start-btn', 'pause': '.player-pause', 'play': '.player-play', 'switchPlayStatus': '.player-play', 'playbackRate': function () {}, 'currentTime': function () {}, 'addCurrentTime': '.add-currenttime', 'subtractCurrentTime': '.subtract-currenttime', /* 当前域名下需包含的路径信息,默认整个域名下所有路径可用 必须是正则 */ include: /^.*/, /* 当前域名下需排除的路径信息,默认不排除任何路径 必须是正则 */ exclude: /\t/ }, 'youtube.com': { // 'webFullScreen': 'button.ytp-size-button', 'fullScreen': 'button.ytp-fullscreen-button' }, 'netflix.com': { 'fullScreen': 'button.button-nfplayerFullscreen', 'addCurrentTime': 'button.button-nfplayerFastForward', 'subtractCurrentTime': 'button.button-nfplayerBackTen' }, 'bilibili.com': { 'fullScreen': '[data-text="进入全屏"]', 'webFullScreen': '[data-text="网页全屏"]', 'autoPlay': '.bilibili-player-video-btn-start', 'switchPlayStatus': '.bilibili-player-video-btn-start' }, 'live.bilibili.com': { 'fullScreen': '.bilibili-live-player-video-controller-fullscreen-btn button', 'webFullScreen': '.bilibili-live-player-video-controller-web-fullscreen-btn button', 'switchPlayStatus': '.bilibili-live-player-video-controller-start-btn button' }, 'iqiyi.com': { 'fullScreen': '.iqp-btn-fullscreen', 'webFullScreen': '.iqp-btn-webscreen' }, 'youku.com': { 'fullScreen': '.control-fullscreen-icon' }, 'ted.com': { 'fullScreen': 'button.Fullscreen' }, /** * 获取域名 , 目前实现方式不好,需改造,对地区性域名(如com.cn)、三级及以上域名支持不好 * */ getDomain: function () { let host = window.location.host let domain = host let tmpArr = host.split('.') if (tmpArr.length > 2) { tmpArr.shift() domain = tmpArr.join('.') } return domain }, /** * 格式化配置任务 * @param isAll { boolean } -可选 默认只格式当前域名或host下的配置任务,传入true则将所有域名下的任务配置都进行格式化 */ formatTCC: function (isAll) { let t = this let keys = Object.keys(t) let domain = t.getDomain() let host = window.location.host function formatter (item) { let defObj = { include: /^.*/, exclude: /\t/ } item.include = item.include || defObj.include item.exclude = item.exclude || defObj.exclude return item } let result = {} keys.forEach(function (key) { let item = t[key] if (isObj(item)) { if (isAll) { item = formatter(item) result[key] = item } else { if (key === host || key === domain) { item = formatter(item) result[key] = item } } } }) return result }, /* 判断所提供的配置任务是否适用于当前URL */ isMatch: function (taskConf) { let url = window.location.href let isMatch = false if (taskConf.include.test(url)) { isMatch = true } if (taskConf.exclude.test(url)) { isMatch = false } return isMatch }, /** * 获取任务配置,只能获取到当前域名下的任务配置信息 * @param taskName {string} -可选 指定具体任务,默认返回所有类型的任务配置 */ getTaskConfig: function () { let t = this if (!t._hasFormatTCC_) { t.formatTCC() t._hasFormatTCC_ = true } let domain = t.getDomain() let taskConf = t[window.location.host] || t[domain] if (taskConf && t.isMatch(taskConf)) { return taskConf } return {} }, /** * 执行当前页面下的相应任务 * @param taskName {object|string} -必选,可直接传入任务配置对象,也可用是任务名称的字符串信息,自己去查找是否有任务需要执行 */ doTask: function (taskName) { let t = this let isDo = false if (!taskName) return isDo let taskConf = isObj(taskName) ? taskName : t.getTaskConfig() if (!isObj(taskConf) || !taskConf[taskName]) return isDo let task = taskConf[taskName] let wrapDom = h5Player.getPlayerWrapDom() if (getType(task) === 'function') { task(h5Player, taskConf) isDo = true } else { /* 触发选择器上的点击事件 */ if (wrapDom && wrapDom.querySelector(task)) { // 在video的父元素里查找,是为了尽可能兼容多实例下的逻辑 wrapDom.querySelector(task).click() isDo = true } else if (document.querySelector(task)) { document.querySelector(task).click() isDo = true } } return isDo } } /** * 元素监听器 * @param selector -必选 * @param fn -必选,元素存在时的回调 * @param shadowRoot -可选 指定监听某个shadowRoot下面的DOM元素 * 参考:https://javascript.ruanyifeng.com/dom/mutationobserver.html */ function ready (selector, fn, shadowRoot) { let listeners = [] let win = window let doc = shadowRoot || win.document let MutationObserver = win.MutationObserver || win.WebKitMutationObserver let observer function $ready (selector, fn) { // 储存选择器和回调函数 listeners.push({ selector: selector, fn: fn }) if (!observer) { // 监听document变化 observer = new MutationObserver(check) observer.observe(shadowRoot || doc.documentElement, { childList: true, subtree: true }) } // 检查该节点是否已经在DOM中 check() } function check () { for (let i = 0; i < listeners.length; i++) { var listener = listeners[i] var elements = doc.querySelectorAll(listener.selector) for (let j = 0; j < elements.length; j++) { var element = elements[j] if (!element._isMutationReady_) { element._isMutationReady_ = true listener.fn.call(element, element) } } } } $ready(selector, fn) } /** * 某些网页用了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 */ function hackAttachShadow () { if (window._hasHackAttachShadow_) return try { window._shadowDomList_ = [] window.Element.prototype._attachShadow = window.Element.prototype.attachShadow window.Element.prototype.attachShadow = function () { let arg = arguments if (arg[0] && arg[0]['mode']) { // 强制使用 open mode arg[0]['mode'] = 'open' } let shadowRoot = this._attachShadow.apply(this, arg) // 存一份shadowDomList window._shadowDomList_.push(shadowRoot) // 在document下面添加 addShadowRoot 自定义事件 let shadowEvent = new window.CustomEvent('addShadowRoot', { shadowRoot, detail: { shadowRoot, message: 'addShadowRoot', time: new Date() }, bubbles: true, cancelable: true }) document.dispatchEvent(shadowEvent) return shadowRoot } window._hasHackAttachShadow_ = true } catch (e) { console.error('hackAttachShadow error by h5player plug-in') } } hackAttachShadow() /* 事件侦听hack */ function hackEventListener () { const EVENT = window.EventTarget.prototype if (EVENT._addEventListener) return EVENT._addEventListener = EVENT.addEventListener EVENT._removeEventListener = EVENT.removeEventListener window._listenerList_ = [] // hack addEventListener EVENT.addEventListener = function () { let arg = arguments let type = arg[0] let listener = arg[1] this._addEventListener.apply(this, arg) this._listeners = this._listeners || {} this._listeners[type] = this._listeners[type] || [] let listenerObj = { target: this, type, listener, options: arg[2], addTime: new Date().getTime() } window._listenerList_.push(listenerObj) this._listeners[type].push(listenerObj) } // hack removeEventListener EVENT.removeEventListener = function () { let arg = arguments let type = arg[0] let listener = arg[1] this._removeEventListener.apply(this, arg) this._listeners = this._listeners || {} this._listeners[type] = this._listeners[type] || [] let result = [] this._listeners[type].forEach(function (listenerObj) { if (listenerObj.listener !== listener) { result.push(listenerObj) } }) this._listeners[type] = result } } hackEventListener() let quickSort = function (arr) { if (arr.length <= 1) { return arr } var pivotIndex = Math.floor(arr.length / 2) var pivot = arr.splice(pivotIndex, 1)[0] var left = [] var right = [] for (var i = 0; i < arr.length; i++) { if (arr[i] < pivot) { left.push(arr[i]) } else { right.push(arr[i]) } } return quickSort(left).concat([pivot], quickSort(right)) } /** * 向上查找操作 * @param dom {Element} -必选 初始dom元素 * @param fn {function} -必选 每一级ParentNode的回调操作 * 如果函数返回true则表示停止向上查找动作 */ function eachParentNode (dom, fn) { let parent = dom.parentNode while (parent) { let isEnd = fn(parent, dom) parent = parent.parentNode if (isEnd) { break } } } /** * 准确地获取对象的具体类型 * @param obj { all } -必选 要判断的对象 * @returns {*} 返回判断的具体类型 */ function getType (obj) { if (obj == null) { return String(obj) } return typeof obj === 'object' || typeof obj === 'function' ? (obj.constructor && obj.constructor.name && obj.constructor.name.toLowerCase()) || /function\s(.+?)\(/.exec(obj.constructor)[1].toLowerCase() : typeof obj } function isObj (obj) { return getType(obj) === 'object' } /** * 深度合并两个可枚举的对象 * @param objA {object} -必选 对象A * @param objB {object} -必选 对象B * @param concatArr {boolean} -可选 合并数组,默认遇到数组的时候,直接以另外一个数组替换当前数组,将此设置true则,遇到数组的时候一律合并,而不是直接替换 * @returns {*|void} */ function mergeObj (objA, objB, concatArr) { function isObj (obj) { return Object.prototype.toString.call(obj) === '[object Object]' } function isArr (arr) { return Object.prototype.toString.call(arr) === '[object Array]' } if (!isObj(objA) || !isObj(objB)) return objA function deepMerge (objA, objB) { let keys = Object.keys(objB) keys.forEach(function (key) { let subItemA = objA[key] let subItemB = objB[key] if (typeof subItemA === 'undefined') { objA[key] = subItemB } else { if (isObj(subItemA) && isObj(subItemB)) { /* 进行深层合并 */ objA[key] = deepMerge(subItemA, subItemB) } else { if (concatArr && isArr(subItemA) && isArr(subItemB)) { objA[key] = subItemA.concat(subItemB) } else { objA[key] = subItemB } } } }) return objA } return deepMerge(objA, objB) } /** * 多对象深度合并,合并规则基于mergeObj,但不存在concatArr选项 * @returns {*} */ function merge () { let result = arguments[0] for (var i = 0; i < arguments.length; i++) { if (i) { result = mergeObj(result, arguments[i]) } } return result } /* ua信息伪装 */ function fakeUA (ua) { Object.defineProperty(navigator, 'userAgent', { value: ua, writable: false, configurable: false, enumerable: true }) } /* ua信息来源:https://developers.whatismybrowser.com */ const userAgentMap = { android: { chrome: 'Mozilla/5.0 (Linux; Android 9; SM-G960F Build/PPR1.180610.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.157 Mobile Safari/537.36', firefox: 'Mozilla/5.0 (Android 7.0; Mobile; rv:57.0) Gecko/57.0 Firefox/57.0' }, iPhone: { safari: 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Mobile/15E148 Safari/604.1', chrome: 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/74.0.3729.121 Mobile/15E148 Safari/605.1' }, iPad: { safari: 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Mobile/15E148 Safari/604.1', chrome: 'Mozilla/5.0 (iPad; CPU OS 12_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/74.0.3729.155 Mobile/15E148 Safari/605.1' } } const fakeConfig = { // 'tv.cctv.com': userAgentMap.iPhone.chrome, // 'v.qq.com': userAgentMap.iPad.chrome, 'open.163.com': userAgentMap.iPhone.chrome, 'm.open.163.com': userAgentMap.iPhone.chrome } function debugMsg () { let arg = Array.from(arguments) arg.unshift('h5player debug message :') console.info.apply(console, arg) } let h5Player = { /* 提示文本的字号 */ fontSize: 16, enable: true, globalMode: true, playerInstance: null, scale: 1, playbackRate: 1, /* 快进快退步长 */ skipStep: 5, /* 获取当前播放器的实例 */ player: function () { let t = this return t.playerInstance || t.getPlayerList()[0] }, /* 每个网页可能存在的多个video播放器 */ getPlayerList: function () { let list = [] function findPlayer (context) { context.querySelectorAll('video').forEach(function (player) { list.push(player) }) } findPlayer(document) // 被封装在 shadow dom 里面的video if (window._shadowDomList_) { window._shadowDomList_.forEach(function (shadowRoot) { findPlayer(shadowRoot) }) } return list }, getPlayerWrapDom: function () { let t = this let player = t.player() if (!player) return let wrapDom = null let playerBox = player.getBoundingClientRect() eachParentNode(player, function (parent) { if (parent === document || !parent.getBoundingClientRect) return let parentBox = parent.getBoundingClientRect() if (parentBox.width && parentBox.height) { if (parentBox.width === playerBox.width && parentBox.height === playerBox.height) { wrapDom = parent } } }) return wrapDom }, /** * 初始化播放器实例 * @param isSingle 是否为单实例video标签 */ initPlayerInstance: function (isSingle) { let t = this if (!t.playerInstance) return let player = t.playerInstance t.filter.reset() t.initTips() t.initPlaybackRate() t.isFoucs() /* 增加通用全屏,网页全屏api */ player._fullScreen_ = new FullScreen(player) player._fullPageScreen_ = new FullScreen(player, true) if (!player._hasCanplayEvent_) { player.addEventListener('canplay', function (event) { t.initAutoPlay(player) }) player._hasCanplayEvent_ = true } /* 播放的时候进行相关同步操作 */ if (!player._hasPlayingInitEvent_) { let setPlaybackRateOnPlayingCount = 0 player.addEventListener('playing', function (event) { if (setPlaybackRateOnPlayingCount === 0) { /* 同步之前设定的播放速度 */ t.setPlaybackRate() if (isSingle === true) { /* 恢复播放进度和进行进度记录 */ t.setPlayProgress(player) setTimeout(function () { t.playProgressRecorder(player) }, 1000 * 3) } } else { t.setPlaybackRate(null, true) } setPlaybackRateOnPlayingCount += 1 }) player._hasPlayingInitEvent_ = true } }, initPlaybackRate: function () { let t = this t.playbackRate = t.getPlaybackRate() }, getPlaybackRate: function () { let t = this let playbackRate = window.localStorage.getItem('_h5_player_playback_rate_') || t.playbackRate return Number(Number(playbackRate).toFixed(1)) }, /* 设置播放速度 */ setPlaybackRate: function (num, notips) { let taskConf = TCC.getTaskConfig() if (taskConf.playbackRate) { TCC.doTask('playbackRate') return } let t = this let player = t.player() let curPlaybackRate if (num) { num = Number(num) if (Number.isNaN(num)) { console.error('h5player: 播放速度转换出错') return false } if (num <= 0) { num = 0.1 } num = Number(num.toFixed(1)) curPlaybackRate = num } else { curPlaybackRate = t.getPlaybackRate() } /* 记录播放速度的信息 */ window.localStorage.setItem('_h5_player_playback_rate_', curPlaybackRate) t.playbackRate = curPlaybackRate player.playbackRate = curPlaybackRate if (!notips) { /* 本身处于1被播放速度的时候不再提示 */ if (!num && curPlaybackRate === 1) return t.tips('播放速度:' + player.playbackRate + '倍') } }, /** * 初始化自动播放逻辑 * 必须是配置了自动播放按钮选择器得的才会进行自动播放 */ initAutoPlay: function (p) { let t = this let player = p || t.player() // 在轮询重试的时候,如果实例变了,或处于隐藏页面中则不进行自动播放操作 if (!player || (p && p !== t.player()) || document.hidden) return let taskConf = TCC.getTaskConfig() if (player && taskConf.autoPlay && player.paused) { TCC.doTask('autoPlay') if (player.paused) { // 轮询重试 if (!player._initAutoPlayCount_) { player._initAutoPlayCount_ = 1 } player._initAutoPlayCount_ += 1 if (player._initAutoPlayCount_ >= 10) { return false } setTimeout(function () { t.initAutoPlay(player) }, 200) } } }, setWebFullScreen: function () { let t = this let player = t.player() let isDo = TCC.doTask('webFullScreen') if (!isDo && player && player._fullPageScreen_) { player._fullPageScreen_.toggle() } }, setCurrentTime: function (num) { if (!num) return num = Number(num) let _num = Math.abs(Number(num.toFixed(1))) let t = this let player = t.player() let taskConf = TCC.getTaskConfig() if (taskConf.currentTime) { TCC.doTask('currentTime') return } if (num > 0) { if (taskConf.addCurrentTime) { TCC.doTask('addCurrentTime') } else { player.currentTime += _num t.tips('前进:' + _num + '秒') } } else { if (taskConf.subtractCurrentTime) { TCC.doTask('subtractCurrentTime') } else { player.currentTime -= _num t.tips('后退:' + _num + '秒') } } }, setVolume: function (num) { if (!num) return num = Number(num) let _num = Math.abs(Number(num.toFixed(2))) let t = this let player = t.player() if (num > 0) { if (player.volume < 1) { player.volume += _num } } else { if (player.volume > 0) { player.volume -= _num } } t.tips('音量:' + parseInt(player.volume * 100) + '%') }, setFakeUA (ua) { ua = ua || userAgentMap.iPhone.safari /* 记录设定的ua信息 */ window.localStorage.setItem('_h5_player_user_agent_', ua) fakeUA(ua) }, /* ua伪装切换开关 */ switchFakeUA (ua) { let customUA = window.localStorage.getItem('_h5_player_user_agent_') if (customUA) { window.localStorage.removeItem('_h5_player_user_agent_') } else { this.setFakeUA(ua) } debugMsg('ua', navigator.userAgent) }, switchPlayStatus: function () { let t = this let player = t.player() let taskConf = TCC.getTaskConfig() if (taskConf.switchPlayStatus) { TCC.doTask('switchPlayStatus') return } if (player.paused) { if (taskConf.play) { TCC.doTask('play') } else { player.play() t.tips('播放') } } else { if (taskConf.pause) { TCC.doTask('pause') } else { player.pause() t.tips('暂停') } } }, tipsClassName: 'html_player_enhance_tips', tips: function (str) { let t = h5Player let player = t.player() if (!player) { console.log('h5Player Tips:', str) return true } let parentNode = player.parentNode // 修复部分提示按钮位置异常问题 let backupStyle = parentNode.getAttribute('style-backup') || '' let defStyle = parentNode.getAttribute('style') || '' if (backupStyle === null) { parentNode.setAttribute('style-backup', defStyle) backupStyle = defStyle } let newStyleArr = backupStyle.split(';') let oldPosition = parentNode.getAttribute('def-position') || window.getComputedStyle(parentNode).position if (parentNode.getAttribute('def-position') === null) { parentNode.setAttribute('def-position', oldPosition || '') } if (['static', 'inherit', 'initial', 'unset', ''].includes(oldPosition)) { newStyleArr.push('position: relative') } let playerBox = player.getBoundingClientRect() newStyleArr.push('min-width:' + playerBox.width + 'px') newStyleArr.push('min-height:' + playerBox.height + 'px') parentNode.setAttribute('style', newStyleArr.join(';')) let tipsSelector = '.' + t.tipsClassName let tipsDom = parentNode.querySelector(tipsSelector) /* 提示dom未初始化的,则进行初始化 */ if (!tipsDom) { t.initTips() tipsDom = parentNode.querySelector(tipsSelector) if (!tipsDom) { console.log('init h5player tips dom error...') return false } } let style = tipsDom.style tipsDom.innerText = str for (var i = 0; i < 3; i++) { if (this.on_off[i]) clearTimeout(this.on_off[i]) } function showTips () { style.display = 'block' t.on_off[0] = setTimeout(function () { style.opacity = 1 }, 50) t.on_off[1] = setTimeout(function () { // 隐藏提示框和还原样式 style.opacity = 0 style.display = 'none' parentNode.setAttribute('style', backupStyle) }, 2000) } if (style.display === 'block') { style.display = 'none' clearTimeout(this.on_off[3]) t.on_off[2] = setTimeout(function () { showTips() }, 100) } else { showTips() } }, /* 设置提示DOM的样式 */ initTips: function () { let t = this let player = t.player() let parentNode = player.parentNode if (parentNode.querySelector('.' + t.tipsClassName)) return let tipsStyle = ` position: absolute; z-index: 999999; font-size: ${t.fontSize || 16}px; padding: 10px; background: rgba(0,0,0,0.4); color:white;top: 50%; left: 50%; transform: translate(-50%,-50%); transition: all 500ms ease; opacity: 0; border-radius:3px; display: none; -webkit-font-smoothing: subpixel-antialiased; font-family: 'microsoft yahei', Verdana, Geneva, sans-serif; -webkit-user-select: none; ` let tips = document.createElement('div') tips.setAttribute('style', tipsStyle) tips.setAttribute('class', t.tipsClassName) parentNode.appendChild(tips) }, on_off: new Array(3), rotate: 0, fps: 30, /* 滤镜效果 */ filter: { key: new Array(5), setup: function () { var view = 'brightness({0}) contrast({1}) saturate({2}) hue-rotate({3}deg) blur({4}px)' for (var i = 0; i < 5; i++) { view = view.replace('{' + i + '}', String(this.key[i])) this.key[i] = Number(this.key[i]) } h5Player.player().style.WebkitFilter = view }, reset: function () { this.key[0] = 1 this.key[1] = 1 this.key[2] = 1 this.key[3] = 0 this.key[4] = 0 this.setup() } }, _isFoucs: false, /* 播放器的聚焦事件 */ isFoucs: function () { let t = h5Player let player = t.player() if (!player) return player.onmouseenter = function (e) { h5Player._isFoucs = true } player.onmouseleave = function (e) { h5Player._isFoucs = false } }, keyList: [13, 16, 17, 18, 27, 32, 37, 38, 39, 40, 49, 50, 51, 52, 67, 68, 69, 70, 73, 74, 75, 79, 81, 82, 83, 84, 85, 87, 88, 89, 90, 97, 98, 99, 100, 220], keyMap: { 'enter': 14, 'shift': 16, 'ctrl': 17, 'alt': 18, 'esc': 27, 'space': 32, '←': 37, '↑': 38, '→': 39, '↓': 40, '1': 49, '2': 50, '3': 51, '4': 52, 'c': 67, 'd': 68, 'e': 69, 'f': 70, 'i': 73, 'j': 74, 'k': 75, 'o': 79, 'q': 81, 'r': 82, 's': 83, 't': 84, 'u': 85, 'w': 87, 'x': 88, 'y': 89, 'z': 90, 'pad1': 97, 'pad2': 98, 'pad3': 99, 'pad4': 100, '\\': 220 }, /* 播放器事件响应器 */ palyerTrigger: function (player, event) { if (!player || !event) return let t = h5Player let keyCode = event.keyCode if (event.shiftKey && !event.ctrlKey && !event.altKey) { // 网页全屏 if (keyCode === 13) { t.setWebFullScreen() } // 视频画面缩放相关事件 let isScaleKeyCode = keyCode === 88 || keyCode === 67 || keyCode === 90 if (!isScaleKeyCode) return // shift+X:视频缩小 -0.1 if (keyCode === 88) { t.scale -= 0.1 } // shift+C:视频放大 +0.1 if (keyCode === 67) { t.scale = Number(t.scale) t.scale += 0.1 } // shift+Z:视频恢复正常大小 if (keyCode === 90) { t.scale = 1 } let scale = t.scale = t.scale.toFixed(1) player.style.transform = 'scale(' + scale + ')' t.tips('视频缩放率:' + (scale * 100) + '%') // 阻止事件冒泡 event.stopPropagation() event.preventDefault() return true } // 防止其它无关组合键冲突 if (event.altKey || event.ctrlKey || event.shiftKey) return // 方向键右→:快进3秒 if (keyCode === 39) { t.setCurrentTime(t.skipStep) } // 方向键左←:后退3秒 if (keyCode === 37) { t.setCurrentTime(-t.skipStep) } // 方向键上↑:音量升高 1% if (keyCode === 38) { t.setVolume(0.01) } // 方向键下↓:音量降低 1% if (keyCode === 40) { t.setVolume(-0.01) } // 空格键:暂停/播放 if (keyCode === 32) { t.switchPlayStatus() } // 按键X:减速播放 -0.1 if (keyCode === 88) { if (player.playbackRate > 0) { t.setPlaybackRate(player.playbackRate - 0.1) } } // 按键C:加速播放 +0.1 if (keyCode === 67) { if (player.playbackRate < 16) { t.setPlaybackRate(player.playbackRate + 0.1) } } // 按键Z:正常速度播放 if (keyCode === 90) { player.playbackRate = 1 t.setPlaybackRate(player.playbackRate) } // 按1-4设置播放速度 49-52;97-100 if ((keyCode >= 49 && keyCode <= 52) || (keyCode >= 97 && keyCode <= 100)) { player.playbackRate = Number(event.key) t.setPlaybackRate(player.playbackRate) } // 按键F:下一帧 if (keyCode === 70) { if (window.location.hostname === 'www.netflix.com') { /* netflix 的F键是全屏的意思 */ return } if (!player.paused) player.pause() player.currentTime += Number(1 / t.fps) t.tips('定位:下一帧') } // 按键D:上一帧 if (keyCode === 68) { if (!player.paused) player.pause() player.currentTime -= Number(1 / t.fps) t.tips('定位:上一帧') } // 按键E:亮度增加% if (keyCode === 69) { t.filter.key[0] += 0.1 t.filter.key[0] = t.filter.key[0].toFixed(2) t.filter.setup() t.tips('图像亮度增加:' + parseInt(t.filter.key[0] * 100) + '%') } // 按键W:亮度减少% if (keyCode === 87) { if (t.filter.key[0] > 0) { t.filter.key[0] -= 0.1 t.filter.key[0] = t.filter.key[0].toFixed(2) t.filter.setup() } t.tips('图像亮度减少:' + parseInt(t.filter.key[0] * 100) + '%') } // 按键T:对比度增加% if (keyCode === 84) { t.filter.key[1] += 0.1 t.filter.key[1] = t.filter.key[1].toFixed(2) t.filter.setup() t.tips('图像对比度增加:' + parseInt(t.filter.key[1] * 100) + '%') } // 按键R:对比度减少% if (keyCode === 82) { if (t.filter.key[1] > 0) { t.filter.key[1] -= 0.1 t.filter.key[1] = t.filter.key[1].toFixed(2) t.filter.setup() } t.tips('图像对比度减少:' + parseInt(t.filter.key[1] * 100) + '%') } // 按键U:饱和度增加% if (keyCode === 85) { t.filter.key[2] += 0.1 t.filter.key[2] = t.filter.key[2].toFixed(2) t.filter.setup() t.tips('图像饱和度增加:' + parseInt(t.filter.key[2] * 100) + '%') } // 按键Y:饱和度减少% if (keyCode === 89) { if (t.filter.key[2] > 0) { t.filter.key[2] -= 0.1 t.filter.key[2] = t.filter.key[2].toFixed(2) t.filter.setup() } t.tips('图像饱和度减少:' + parseInt(t.filter.key[2] * 100) + '%') } // 按键O:色相增加 1 度 if (keyCode === 79) { t.filter.key[3] += 1 t.filter.setup() t.tips('图像色相增加:' + t.filter.key[3] + '度') } // 按键I:色相减少 1 度 if (keyCode === 73) { t.filter.key[3] -= 1 t.filter.setup() t.tips('图像色相减少:' + t.filter.key[3] + '度') } // 按键K:模糊增加 1 px if (keyCode === 75) { t.filter.key[4] += 1 t.filter.setup() t.tips('图像模糊增加:' + t.filter.key[4] + 'PX') } // 按键J:模糊减少 1 px if (keyCode === 74) { if (t.filter.key[4] > 0) { t.filter.key[4] -= 1 t.filter.setup() } t.tips('图像模糊减少:' + t.filter.key[4] + 'PX') } // 按键Q:图像复位 if (keyCode === 81) { t.filter.reset() t.tips('图像属性:复位') } // 按键S:画面旋转 90 度 if (keyCode === 83) { t.rotate += 90 if (t.rotate % 360 === 0) t.rotate = 0 player.style.transform = 'rotate(' + t.rotate + 'deg)' t.tips('画面旋转:' + t.rotate + '度') } // 按键回车,进入全屏 if (keyCode === 13) { let isDo = TCC.doTask('fullScreen') if (!isDo && player._fullScreen_) { player._fullScreen_.toggle() } } // 阻止事件冒泡 event.stopPropagation() event.preventDefault() return true }, /* 判断焦点是否处于可编辑元素 */ isEditableTarget: function (target) { let isEditable = target.getAttribute && target.getAttribute('contenteditable') === 'true' let isInputDom = /INPUT|TEXTAREA|SELECT/.test(target.nodeName) return isEditable || isInputDom }, /* 按键响应方法 */ keydownEvent: function (event) { let t = h5Player let keyCode = event.keyCode let player = t.player() /* 处于可编辑元素中不执行任何快捷键 */ if (t.isEditableTarget(event.target)) return /* shift+f 切换UA伪装 */ // if (event.shiftKey && keyCode === 70) { // t.switchFakeUA() // } /* 未用到的按键不进行任何事件监听 */ let isInUseCode = t.keyList.includes(keyCode) if (!isInUseCode) return if (!player) { // console.log('无可用的播放,不执行相关操作') return } /* 切换插件的可用状态 */ if (event.ctrlKey && keyCode === 32) { t.enable = !t.enable if (t.enable) { t.tips('启用h5Player插件') } else { t.tips('禁用h5Player插件') } } if (!t.enable) { console.log('h5Player 已禁用~') return false } // 按ctrl+\ 键进入聚焦或取消聚焦状态,用于视频标签被遮挡的场景 if (event.ctrlKey && keyCode === 220) { t.globalMode = !t.globalMode if (t.globalMode) { t.tips('全局模式') } else { t.tips('禁用全局模式') } } /* 非全局模式下,不聚焦则不执行快捷键的操作 */ if (!t.globalMode && !t._isFoucs) return /* 响应播放器相关操作 */ t.palyerTrigger(player, event) }, /** * 获取播放进度 * @param player -可选 对应的h5 播放器对象, 如果不传,则获取到的是整个播放进度表,传则获取当前播放器的播放进度 */ getPlayProgress: function (player) { let progressMap = window.localStorage.getItem('_h5_player_play_progress_') if (!progressMap) { progressMap = {} } else { progressMap = JSON.parse(progressMap) } if (!player) { return progressMap } else { let keyName = window.location.href || player.src if (progressMap[keyName]) { return progressMap[keyName].progress } else { return player.currentTime } } }, /* 播放进度记录器 */ playProgressRecorder: function (player) { let t = h5Player clearTimeout(player._playProgressTimer_) function recorder (player) { player._playProgressTimer_ = setTimeout(function () { let progressMap = t.getPlayProgress() let keyName = window.location.href || player.src let list = Object.keys(progressMap) /* 只保存最近10个视频的播放进度 */ if (list.length > 10) { /* 根据更新的时间戳,取出最早添加播放进度的记录项 */ let timeList = [] list.forEach(function (keyName) { progressMap[keyName] && progressMap[keyName].t && timeList.push(progressMap[keyName].t) }) timeList = quickSort(timeList) let timestamp = timeList[0] /* 删除最早添加的记录项 */ list.forEach(function (keyName) { if (progressMap[keyName].t === timestamp) { delete progressMap[keyName] } }) } /* 记录当前播放进度 */ progressMap[keyName] = { progress: player.currentTime, t: new Date().getTime() } /* 存储播放进度表 */ window.localStorage.setItem('_h5_player_play_progress_', JSON.stringify(progressMap)) /* 循环侦听 */ recorder(player) }, 1000 * 2) } recorder(player) }, /* 设置播放进度 */ setPlayProgress: function (player, time) { if (!player) return let t = h5Player let curTime = Number(t.getPlayProgress(player)) if (!curTime || Number.isNaN(curTime)) return player.currentTime = curTime || player.currentTime if (curTime > 3) { t.tips('为你恢复上次播放进度~') } }, /** * 检测h5播放器是否存在 * @param callback */ detecH5Player: function () { let t = this let playerList = t.getPlayerList() if (playerList.length) { console.log('检测到HTML5视频!') /* 单video实例标签的情况 */ if (playerList.length === 1) { t.playerInstance = playerList[0] t.initPlayerInstance(true) } else { /* 多video实例标签的情况 */ playerList.forEach(function (player) { /* 鼠标移到其上面的时候重新指定实例 */ if (player._hasMouseRedirectEvent_) return player.addEventListener('mouseenter', function (event) { t.playerInstance = event.target t.initPlayerInstance(false) }) player._hasMouseRedirectEvent_ = true /* 播放器开始播放的时候重新指向实例 */ if (player._hasPlayingRedirectEvent_) return player.addEventListener('playing', function (event) { t.playerInstance = event.target t.initPlayerInstance(false) /* 同步之前设定的播放速度 */ t.setPlaybackRate() }) player._hasPlayingRedirectEvent_ = true }) } } }, /* 绑定相关事件 */ bindEvent: function () { var t = this if (t._hasBindEvent_) return document.removeEventListener('keydown', t.keydownEvent) document.addEventListener('keydown', t.keydownEvent, true) /* 兼容iframe操作 */ if (window.top !== window && window.top.document) { window.top.document.removeEventListener('keydown', t.keydownEvent) window.top.document.addEventListener('keydown', t.keydownEvent, true) } t._hasBindEvent_ = true }, init: function (global) { var t = this if (global) { /* 绑定键盘事件 */ t.bindEvent() /** * 判断是否需要进行ua伪装 * 下面方案暂时不可用 * 由于部分网站跳转至移动端后域名不一致,形成跨域问题 * 导致无法同步伪装配置而不断死循环跳转 * eg. open.163.com * */ // let customUA = window.localStorage.getItem('_h5_player_user_agent_') // debugMsg(customUA, window.location.href, window.navigator.userAgent, document.referrer) // if (customUA) { // t.setFakeUA(customUA) // alert(customUA) // } else { // alert('ua false') // } /* 对配置了ua伪装的域名进行伪装 */ let host = window.location.host if (fakeConfig[host]) { t.setFakeUA(fakeConfig[host]) } } else { /* 检测是否存在H5播放器 */ t.detecH5Player() } }, load: false } try { /* 初始化全局所需的相关方法 */ h5Player.init(true) /* 检测到有视频标签就进行初始化 */ ready('video', function () { h5Player.init() }) /* 检测shadow dom 下面的video */ document.addEventListener('addShadowRoot', function (e) { let shadowRoot = e.detail.shadowRoot ready('video', function (element) { h5Player.init() }, shadowRoot) }) window.top._h5PlayerForDebug_ = h5Player } catch (e) { console.error('h5player:', e) } // document.addEventListener('visibilitychange', function () { // if (!document.hidden) { // h5Player.initAutoPlay() // } // }) })()