// ==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
// ==/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()
// }
// })
})()