├── docs ├── CNAME ├── icons │ ├── play.svg │ ├── pause.svg │ ├── web-fullscreen.svg │ ├── volume-ok.svg │ ├── fullscreen.svg │ ├── volume-x.svg │ ├── later.svg │ ├── emoj.svg │ ├── danmu-ok.svg │ ├── danmu-op.svg │ └── danmu-x.svg ├── game.html ├── index.html ├── danmaku.js ├── mug.js └── eplayer.js ├── .gitignore ├── package.json └── README.md /docs/CNAME: -------------------------------------------------------------------------------- 1 | eplayer.js.org -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode/ 3 | .idea/ 4 | npm-debug.log 5 | yarn-error.log 6 | package-lock.json 7 | yarn.lock 8 | .DS_Store 9 | .cache/ -------------------------------------------------------------------------------- /docs/icons/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/icons/pause.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/icons/web-fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/icons/volume-ok.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/icons/fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/icons/volume-x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/game.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/icons/later.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eplayer", 3 | "version": "1.6.11", 4 | "description": "A web-components html5 video player facing future", 5 | "main": "./docs/eplayer.js", 6 | "module": "./docs/eplayer.js", 7 | "unpkg": "./docs/eplayer.js", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "build": "rollup -c", 11 | "start": "serve docs" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/132yse/eplayer.git" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/132yse/eplayer/issues" 22 | }, 23 | "homepage": "https://github.com/132yse/eplayer#readme", 24 | "dependencies": { 25 | "pixi-filters": "^6.0.4", 26 | "pixi.js": "^8.1.8", 27 | "serve": "^14.2.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/icons/emoj.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/icons/danmu-ok.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/icons/danmu-op.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/icons/danmu-x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | eplayer 9 | 10 | 11 | 12 | 13 | 14 | 65 | 66 |
67 | 70 | 71 |
72 | 73 | 74 | 75 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

eplayer logo

2 | 3 | # eplayer [![NPM version](https://img.shields.io/npm/v/eplayer.svg?style=flat-square)](https://npmjs.com/package/eplayer) [![NPM downloads](https://img.shields.io/npm/dt/eplayer.svg?style=flat-square)](https://npmjs.com/package/eplayer) 4 | 5 | :dart: A web-components html5 video player facing future. 6 | 7 | #### who use eplayer? 8 | 9 | [eplayer.js.org - demo](https://eplayer.js.org/) 10 | 11 | [clicli - C 站](https://www.clicli.me/) 12 | 13 | #### Contributors 14 | 15 | 感谢大家的 pr,阿里嘎多! 16 | 17 | 18 | 19 | ### Use 20 | 21 | 0. ep 基于 web-component,为了兼容,需要事先引入 polyfill 22 | 23 | ```html 24 | 25 | ``` 26 | 27 | 1. 插入 `e-player` 标签,没错,只需要将平时用的 `video` 换成 `e-player` 即可 28 | 29 | ```html 30 | 31 | ``` 32 | 33 | type 属性可选,默认为 mp4,支持 hls 和 flv 34 | 35 | 2. 注册 `customElement`,注意 `type=module`,使用 es6 的 import 36 | 37 | ```html 38 | 43 | ``` 44 | 45 | 3. 可选定制 css,用于穿透 shadow-dom 预留的默认样式 46 | 47 | ```css 48 | e-player { 49 | /* 主题色 默认为 clicli 同款基佬紫*/ 50 | --theme: #00fff6; 51 | /* 进度条底色 一般不用设置 */ 52 | --progress: rgba(255, 255, 255, 0.3); 53 | /* 进度条偏移颜色 一般不用设置 */ 54 | --buffer: rgba(255, 255, 255, 0.6); 55 | /* 图标颜色 一般不用设置 */ 56 | --icons: rgba(255, 255, 255, 0.6); 57 | } 58 | ``` 59 | 60 | 4. 可选定制插件,会在右击菜单中出现一个选项,触发点击事件 61 | 62 | ```js 63 | Eplayer.use('github源码', ep => { 64 | // ep 为 shdow-root 元素 65 | window.location.href = 'https://github.com/132yse/eplayer' 66 | }) 67 | ``` 68 | 69 | #### hls 70 | 71 | 原生支持 `mp4` 和 `mkv` ,如果需要支持 `m3u8`,需要先引入 `hls.js` 72 | 73 | ```html 74 | 75 | ``` 76 | 77 | ### Npm 78 | 79 | ```shell 80 | yarn add eplayer -S 81 | ``` 82 | 83 | 同样的注册 customElement,但是注意,customElement 只能注册一次,然后还没完,往下看: 84 | 85 | #### omim 86 | 87 | omim 是腾讯前端框架 [omi](https://github.com/Tencent/omi) 的组件库分支,eplayer 已经集成进去 88 | 89 | [戳我戳我](https://github.com/Tencent/omi/tree/master/packages/omim/demos/player) 90 | 91 | #### Vue 92 | 93 | vue 默认是不支持 web-components 的,它无法主动判断含有`-`的是 vue 组件还是 web 组件 94 | 95 | 所以需要手动配置,忽略掉`e-player` 96 | 97 | ```JavaScript 98 | Vue.config.ignoredElements = [ 99 | 'e-player' 100 | ] 101 | ``` 102 | 103 | 然后,同样需要引入上面的几个文件,然后 bind 一下 src 和 type 104 | 105 | ```html 106 | 107 | ``` 108 | 109 | 可以封装成 vue 组件来使用:[vue-web-components-wrapper](https://github.com/vuejs/vue-web-component-wrapper) 110 | 111 | #### React / Fre 112 | 113 | react 直接支持 customElement,直接在 render 函数中`e-player`标签 114 | 115 | 比如,下面这个 fre 的粒子 116 | 117 | ```js 118 | function EPlayer({ src, type }) { 119 | const [url, setUrl] = useState(0) 120 | useEffect(() => { 121 | fetch(`https://jx.clicli.us/jx?url=${src}@dogecloud`) 122 | .then(res => res.json()) 123 | .then(data => { 124 | setUrl(data.url) 125 | }) 126 | }, []) 127 | return 128 | } 129 | ``` 130 | 131 | 完整代码在这里:[fre-eplayer](https://github.com/cliclitv/fre-eplayer) 132 | 133 | #### Angular 134 | 135 | 在 `angular.json` 中添加 `webcomponentsjs` 和 `hls.js` 136 | 137 | ```json 138 | ... 139 | "scripts": [ 140 | "node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js", 141 | "node_modules/hls.js/dist/hls.min.js" 142 | ] 143 | ... 144 | ``` 145 | 146 | 在 `app.component.ts` 中引入 `eplayer` 147 | 148 | ```ts 149 | import { Component, OnInit } from "@angular/core"; 150 | import Eplayer from "eplayer"; 151 | 152 | @Component({ 153 | selector: "app-root", 154 | templateUrl: "./app.component.html", 155 | styleUrls: ["./app.component.scss"], 156 | }) 157 | export class AppComponent implements OnInit { 158 | ngOnInit(): void { 159 | customElements.define("e-player", Eplayer); 160 | } 161 | } 162 | ``` 163 | 164 | 在需要使用 `eplayer` 的模块中启用自定义元素的支持 165 | 166 | ```ts 167 | import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; 168 | import { CommonModule } from "@angular/common"; 169 | import { VideoComponent } from "./video.component"; 170 | 171 | @NgModule({ 172 | declarations: [VideoComponent], 173 | imports: [CommonModule], 174 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 175 | }) 176 | export class VideoModule {} 177 | ``` 178 | 179 | 在相应的 `html` 文件中对 `src` 和 `type` 绑定 180 | 181 | ```html 182 | 183 | ``` 184 | 185 | 完整项目示例: [@cliclitv/pwa](https://github.com/cliclitv/pwa) 186 | 187 | #### ssr 188 | 189 | ssr 服务端没有 web-components 的 API,通常 web-components 的 ssr 都会通过一些库去模拟这些 API 190 | 191 | eplayer 不推荐这么做,请直接和正常的 html 引入方式一样,引入 cdn 192 | 193 | #### WAP 194 | 195 | WAP 端建议使用原生播放器,国产浏览器太乱了,没得兼容,同时建议往 APP 上引流 196 | 197 | #### Screenshot 198 | 199 | ![](https://ww1.sinaimg.cn/mw690/0065Zy9ely1g9srnm3ezpj31jg0v3kjl.jpg) 200 | 201 | -------------------------------------------------------------------------------- /docs/danmaku.js: -------------------------------------------------------------------------------- 1 | const SPEED_ARG = 0.009 2 | 3 | const defaultDanmakuData = { 4 | msg: '', 5 | fontSize: 24, 6 | fontColor: '#ffffff', 7 | rollTime: 0, 8 | } 9 | 10 | class Danmaku { 11 | constructor(options) { 12 | this._container = options.container 13 | this._totalWidth = null 14 | this._totalHeight = null 15 | this._trackSize = 12 16 | this._renderTimer = null 17 | this._queue = [] 18 | this._tracks = null 19 | this._autoId = 0 20 | this._paused = true 21 | this.resize() 22 | this._resetTracks() 23 | console.log(this._container) 24 | } 25 | 26 | resize() { 27 | this._totalWidth = this._container.offsetWidth 28 | this._totalHeight = this._container.offsetHeight 29 | this.clearScreen() 30 | } 31 | 32 | clearScreen() { 33 | this._clearDanmakuNodes() 34 | this._resetTracks() 35 | } 36 | 37 | _resetTracks() { 38 | const count = Math.floor(this._totalHeight / this._trackSize / 3) 39 | this._tracks = new Array(count) 40 | for (let i = 0; i < count; i++) { 41 | this._tracks[i] = [] 42 | } 43 | } 44 | 45 | // 循环弹幕节点 46 | _eachDanmakuNode(fn) { 47 | let child = this._container.firstChild 48 | let id, y 49 | while (child) { 50 | if (child.nodeType === 1) { 51 | y = child.getAttribute('data-y') 52 | id = child.getAttribute('data-id') 53 | if (y && id) { fn(child, Number(y), Number(id)) } 54 | } 55 | child = child.nextSibling 56 | } 57 | } 58 | 59 | _clearDanmakuNodes() { 60 | const nodes = [] 61 | this._eachDanmakuNode((node) => { 62 | nodes.push(node) 63 | }) 64 | nodes.forEach((node) => { 65 | this._container.removeChild(node) 66 | }) 67 | } 68 | 69 | _parseData(data) { 70 | return Object.assign({ 71 | autoId: ++this._autoId, 72 | fontSize: Math.floor(Math.random() * 20) + 20 73 | }, defaultDanmakuData, data) 74 | } 75 | 76 | add(data) { 77 | this._queue.push(this._parseData(data)) 78 | // 如果队列轮询已经停止,则启动 79 | if (!this._renderTimer) { this._render() } 80 | } 81 | 82 | // 把弹幕数据加到合适的轨道 83 | _addToTrack(data) { 84 | // 单条轨道 85 | let track 86 | // 轨道的最后一项弹幕数据 87 | let lastItem 88 | // 弹幕已经走的路程 89 | let distance 90 | // 弹幕数据最终坐落的轨道索引 91 | // 有些弹幕会占多条轨道,所以 y 是个数组 92 | let y = [] 93 | 94 | const now = Date.now() 95 | 96 | for (let i = 0; i < this._tracks.length; i++) { 97 | track = this._tracks[i] || [] 98 | 99 | if (track.length) { 100 | // 轨道被占用,要计算是否会重叠 101 | // 只需要跟轨道最后一条弹幕比较即可 102 | lastItem = track[track.length - 1] 103 | 104 | // 计算已滚动距离 105 | distance = lastItem.rollSpeed * (now - lastItem.startTime) / 1000 106 | 107 | // 通过速度差,计算最后一条弹幕全部消失前,是否会与新增弹幕重叠 108 | // 如果不会重叠,则可以使用当前轨道 109 | if ( 110 | (distance > lastItem.width) && 111 | ( 112 | (data.rollSpeed <= lastItem.rollSpeed) || 113 | ((distance - lastItem.width) / (data.rollSpeed - lastItem.rollSpeed) > 114 | (this._totalWidth + lastItem.width - distance) / lastItem.rollSpeed) 115 | ) 116 | ) { 117 | y.push(i) 118 | } else { 119 | y = [] 120 | } 121 | 122 | } else { 123 | // 轨道未被占用 124 | y.push(i) 125 | } 126 | 127 | // 有足够的轨道可以用时,就可以新增弹幕了,否则等下一次轮询 128 | if (y.length >= data.useTracks) { 129 | data.y = y 130 | y.forEach((i) => { 131 | this._tracks[i].push(data) 132 | }) 133 | break 134 | } 135 | } 136 | } 137 | 138 | // (弹幕飘到尽头后)从轨道中移除对应数据 139 | _removeFromTrack(y, id) { 140 | y.forEach((i) => { 141 | const track = this._tracks[i] 142 | for (let j = 0; j < track.length; j++) { 143 | if (track[j].autoId === id) { 144 | track.splice(j, 1) 145 | break 146 | } 147 | } 148 | }) 149 | } 150 | 151 | // 通过 y 和 id 获取弹幕数据 152 | _findData(y, id) { 153 | const track = this._tracks[y] 154 | for (let j = 0; j < track.length; j++) { 155 | if (track[j].autoId === id) { 156 | return track[j] 157 | } 158 | } 159 | } 160 | 161 | // 轮询渲染 162 | _render() { 163 | if (this._paused) { return } 164 | try { 165 | this._renderToDOM() 166 | } finally { 167 | this._renderEnd() 168 | } 169 | } 170 | 171 | _renderToDOM() { 172 | let count = Math.floor(this._tracks.length / 3), i = 0 173 | 174 | while (count && i < this._queue.length) { 175 | const data = this._queue[i] 176 | let node = data.node 177 | 178 | if (!node) { 179 | // 弹幕节点基本样式 180 | data.node = node = document.createElement('div') 181 | node.innerText = data.msg 182 | node.style.position = 'absolute' 183 | node.style.left = '100%' 184 | node.style.whiteSpace = 'nowrap' 185 | node.style.color = data.fontColor 186 | node.style.fontSize = data.fontSize + 'px' 187 | node.style.willChange = 'transform' 188 | this._container.appendChild(node) 189 | 190 | data.useTracks = Math.ceil(node.offsetHeight / this._trackSize) 191 | // 占用的轨道数多于轨道总数,则忽略此数据 192 | if (data.useTracks > this._tracks.length) { 193 | this._queue.splice(i, 1) 194 | this._container.removeChild(node) 195 | continue 196 | } 197 | 198 | data.width = node.offsetWidth 199 | data.totalDistance = data.width + this._totalWidth 200 | data.rollTime = data.rollTime || 201 | Math.floor(data.totalDistance * SPEED_ARG * (Math.random() * 0.3 + 0.7)) 202 | data.rollSpeed = data.totalDistance / data.rollTime 203 | } 204 | 205 | this._addToTrack(data) 206 | 207 | if (data.y) { 208 | this._queue.splice(i, 1) 209 | 210 | node.setAttribute('data-id', data.autoId) 211 | node.setAttribute('data-y', data.y[0]) 212 | node.style.top = data.y[0] * this._trackSize + 'px' 213 | node.style.transition = `transform ${data.rollTime}s linear` 214 | node.style.transform = `translateX(-${data.totalDistance}px)` 215 | node.addEventListener('transitionstart', () => { 216 | data.startTime = Date.now() 217 | }, false) 218 | node.addEventListener('transitionend', () => { 219 | this._removeFromTrack(data.y, data.autoId) 220 | this._container.removeChild(node) 221 | this._queue = this._queue.filter(item => item.autoId != data.autoId) 222 | }, false) 223 | 224 | data.startTime = Date.now() + 80 225 | 226 | } else { 227 | // 当前弹幕要排队,继续处理下一条 228 | i++ 229 | } 230 | 231 | count-- 232 | } 233 | } 234 | // 轮询结束后,根据队列长度继续执行或停止执行 235 | _renderEnd() { 236 | if (this._queue.length > 0) { 237 | this._renderTimer = requestAnimationFrame(() => { 238 | this._render() 239 | }) 240 | } else { 241 | this._renderTimer = null 242 | } 243 | } 244 | 245 | pause() { 246 | if (this._paused) { return } 247 | this._paused = true 248 | 249 | this._eachDanmakuNode((node, y, id) => { 250 | const data = this._findData(y, id) 251 | if (data) { 252 | // 获取已滚动距离 253 | const transform = getComputedStyle(node, null).getPropertyValue('transform') 254 | data.rolledDistance = -Number(new DOMMatrix(transform).m41) 255 | 256 | // 移除动画,计算出弹幕所在的位置,固定样式 257 | node.style.transition = '' 258 | node.style.transform = `translateX(-${data.rolledDistance}px)` 259 | } 260 | }) 261 | } 262 | 263 | // 继续滚动弹幕 264 | resume() { 265 | if (!this._paused) { return } 266 | 267 | this._eachDanmakuNode((node, y, id) => { 268 | const data = this._findData(y, id) 269 | if (data) { 270 | data.startTime = Date.now() 271 | // 重新计算滚完剩余距离需要多少时间 272 | data.rollTime = (data.totalDistance - data.rolledDistance) / data.rollSpeed 273 | node.style.transition = `transform ${data.rollTime}s linear` 274 | node.style.transform = `translateX(-${data.totalDistance}px)` 275 | } 276 | }) 277 | 278 | this._paused = false 279 | 280 | if (!this._renderTimer) { this._render() } 281 | } 282 | } -------------------------------------------------------------------------------- /docs/mug.js: -------------------------------------------------------------------------------- 1 | const assetUrl = 'https://registry.npmmirror.com/eplayer/1.5.4/files/docs/res' 2 | 3 | class Mug { 4 | constructor(beatMap, container, video) { 5 | this.container = container 6 | this.beatMap = beatMap 7 | this.gameStatus = 0 8 | this.buttonX = 85 9 | 10 | this.buttonArray = [] 11 | this.imgNumber = 1 12 | this.fps = 0 13 | this.score = 0 14 | this.str = "0" 15 | this.app = new PIXI.Application(500, 800) 16 | this.container.appendChild(this.app.view) 17 | console.log(container) 18 | this.app.view.style.width = container.offsetWidth + 'px' 19 | this.video = video 20 | 21 | this.drawUI() 22 | const buttonArray = this.buttonArray 23 | document.onkeydown = function (e) { 24 | if (e && e.keyCode == 68) { 25 | buttonArray[0].keyDown() 26 | } else if (e && e.keyCode == 70) { 27 | buttonArray[1].keyDown() 28 | } else if (e && e.keyCode == 74) { 29 | buttonArray[2].keyDown() 30 | } else if (e && e.keyCode == 75) { 31 | buttonArray[3].keyDown() 32 | } 33 | } 34 | document.onkeyup = function (e) { 35 | if (e && e.keyCode == 68) { 36 | buttonArray[0].keyUp() 37 | } else if (e && e.keyCode == 70) { 38 | buttonArray[1].keyUp() 39 | } else if (e && e.keyCode == 74) { 40 | buttonArray[2].keyUp() 41 | } else if (e && e.keyCode == 75) { 42 | buttonArray[3].keyUp() 43 | } 44 | } 45 | 46 | this.app.ticker.add(this.animate.bind(this)) 47 | } 48 | 49 | animate() { 50 | if (this.gameStatus == 0) { 51 | return 52 | } 53 | 54 | this.fps = this.video.currentTime 55 | //创建动物 56 | const head = this.beatMap[0] 57 | 58 | 59 | if (head && this.fps >= head.fps) { 60 | console.log(this.fps) 61 | this.beatMap.shift() 62 | let button = this.buttonArray[head.button - 1] 63 | button.createAnimal() 64 | } 65 | 66 | //图片移动 67 | for (let i = 0; i < this.buttonArray.length; i++) { 68 | let button = this.buttonArray[i] 69 | button.animalMove() 70 | button.deleteAnimal() 71 | button.scoreMove() 72 | } 73 | 74 | if (!head) { 75 | this.over() 76 | } 77 | } 78 | 79 | drawUI() { 80 | //游戏元素图层 81 | let gameCeng = new PIXI.Container() 82 | this.app.stage.addChild(gameCeng) 83 | //游戏背景 84 | let bg = new PIXI.Sprite.fromImage(`${assetUrl}/beijing.png`) 85 | gameCeng.addChild(bg) 86 | //ui图层 87 | let uiCeng = new PIXI.Container() 88 | this.app.stage.addChild(uiCeng) 89 | this.uiCeng = uiCeng 90 | 91 | //游戏对象层 92 | let gameObjectCeng = new PIXI.Container() 93 | gameCeng.addChild(gameObjectCeng) 94 | 95 | //line 图层 96 | let lineCeng = new PIXI.Container() 97 | gameObjectCeng.addChild(lineCeng) 98 | 99 | //兔子图片图层 100 | let animalCeng = new PIXI.Container() 101 | gameObjectCeng.addChild(animalCeng) 102 | 103 | //score图层 104 | let scoreCeng = new PIXI.Container() 105 | gameObjectCeng.addChild(scoreCeng) 106 | 107 | //点击位置 108 | let touming = new PIXI.Sprite.fromImage(`${assetUrl}/touming.png`) 109 | lineCeng.addChild(touming) 110 | touming.y = 600 111 | touming.x = 250 112 | touming.anchor.set(0.5, 0.5) 113 | 114 | 115 | for (let i = 0; i < 4; i++) { 116 | let button = new Button(this.imgNumber, gameObjectCeng, uiCeng, lineCeng, animalCeng, this.buttonX, this) 117 | this.buttonX = button.bjt.x + 110 118 | this.imgNumber++ 119 | this.buttonArray.push(button) 120 | } 121 | 122 | 123 | let style = { 124 | font: 'bold 40px 微软雅黑',//加粗 倾斜 字号 字体名称 125 | fill: '#F7EDCA',//颜色 126 | stroke: '#4a1850',//描边颜色 127 | strokeThickness: 5,//描边宽度 128 | dropShadow: true,//开启阴影 129 | dropShadowColor: '#000000',//阴影颜色 130 | dropShadowAngle: Math.PI / 6,//阴影角度 131 | dropShadowDistance: 6,//投影距离 132 | wordWrap: true,//开启自动换行(注:开启后在文本中空格处换行,如文本中没有空格则不换行) 133 | wordWrapWidth: 150,//自动换行宽度 134 | } 135 | 136 | let text = new PIXI.Text("得分 ", style) 137 | uiCeng.addChild(text) 138 | text.x = 30 139 | text.y = 30 140 | 141 | //创建文本 142 | let scoreTxt = new PIXI.Text(this.str, style) 143 | this.scoreTxt = scoreTxt 144 | uiCeng.addChild(scoreTxt) 145 | scoreTxt.x = 130 146 | scoreTxt.y = 30 147 | 148 | let startBtn = new PIXI.Sprite.fromImage(`${assetUrl}/kaishianniu.png`) 149 | uiCeng.addChild(startBtn) 150 | startBtn.interactive = true 151 | startBtn.on("pointerdown", () => { 152 | startBtn.visible = false 153 | this.gameStatus = 1 154 | this.video.play() 155 | }) 156 | 157 | startBtn.x = 200 158 | startBtn.y = 345 159 | 160 | } 161 | 162 | over() { 163 | this.video.pause() 164 | let gameoverPanel = new PIXI.Sprite.fromImage(`${assetUrl}/beiban.png`) 165 | this.uiCeng.addChild(gameoverPanel) 166 | gameoverPanel.x = 20 167 | gameoverPanel.y = 100 168 | gameoverPanel.alpha = 0.9 169 | 170 | let style = { 171 | font: 'bold 40px 微软雅黑',//加粗 倾斜 字号 字体名称 172 | fill: '#F7EDCA',//颜色 173 | } 174 | let scoreTxt = new PIXI.Text(this.score, style) 175 | gameoverPanel.addChild(scoreTxt) 176 | scoreTxt.x = 210 177 | scoreTxt.y = 110 178 | 179 | let restartBtn = new PIXI.Sprite.fromImage(`${assetUrl}/fanhuianniu.png`) 180 | gameoverPanel.addChild(restartBtn) 181 | restartBtn.x = 185 182 | restartBtn.y = 330 183 | restartBtn.interactive = true 184 | restartBtn.on("click", function () { 185 | window.location.reload() 186 | }) 187 | restartBtn.on("touchstart", function () { 188 | window.location.reload() 189 | }) 190 | this.gameStatus = 0 191 | 192 | } 193 | 194 | } 195 | 196 | //获取显示对象的世界坐标 197 | function getWorldPosition(displayObject) { 198 | let x = displayObject.transform.worldTransform.tx 199 | let y = displayObject.transform.worldTransform.ty 200 | return { "x": x, "y": y } 201 | } 202 | 203 | class Button { 204 | constructor(imgNumber, gameObjectCeng, uiCeng, lineCeng, animalCeng, buttonX, that) { 205 | this.gameObjectCeng = gameObjectCeng 206 | this.bjt = new PIXI.Sprite.fromImage(`${assetUrl}/bjt${imgNumber}.png`) 207 | gameObjectCeng.addChild(this.bjt) 208 | this.bjt.anchor.set(0.5, 1) 209 | this.bjt.x = buttonX 210 | this.bjt.y = 800 211 | this.bjt.visible = false 212 | this.animalCeng = animalCeng 213 | 214 | this.button = new PIXI.Sprite.fromImage(`${assetUrl}/anniu${imgNumber}.png`) 215 | uiCeng.addChild(this.button) 216 | this.button.anchor.set(0.5, 0.5) 217 | this.button.y = 754 218 | this.button.x = this.bjt.x 219 | this.type = imgNumber 220 | 221 | this.kong = new PIXI.Sprite.fromImage(`${assetUrl}/kong.png`) 222 | lineCeng.addChild(this.kong) 223 | this.kong.anchor.set(0.5, 0.5) 224 | this.kong.x = this.bjt.x 225 | this.kong.y = 600 226 | 227 | this.button.interactive = true 228 | this.animalArray = [] 229 | 230 | this.show = true 231 | 232 | this.scoreArray = [] 233 | 234 | this.soft = that 235 | 236 | // Handlers for button 237 | this.button.on("pointerdown", this.buttonMouseHandler.bind(this)) 238 | this.button.on("pointerup", this.buttonMouseupHandler.bind(this)) 239 | this.button.on("click", this.buttonClick.bind(this)) 240 | } 241 | 242 | createAnimal() { 243 | let animal = new Animal(this.type, this.button.x) 244 | this.animalCeng.addChild(animal.animal) 245 | this.animalArray.push(animal) 246 | } 247 | 248 | animalMove() { 249 | for (let i = 0; i < this.animalArray.length; i++) { 250 | let animal = this.animalArray[i] 251 | animal.move() 252 | } 253 | } 254 | 255 | deleteAnimal() { 256 | for (let i = this.animalArray.length - 1; i >= 0; i--) { 257 | let animal = this.animalArray[i] 258 | if (this.kong.y + 46 < animal.animal.y && this.show == true) { 259 | this.scoreAction("miss") 260 | this.show = false 261 | } 262 | if (animal.animal.y > 800) { 263 | this.show = true 264 | this.animalCeng.removeChild(animal.animal) 265 | this.animalArray.splice(i, 1) 266 | } 267 | } 268 | } 269 | 270 | buttonMouseHandler() { 271 | this.buttonClick() 272 | } 273 | 274 | buttonMouseupHandler() { 275 | this.bjt.visible = false 276 | } 277 | 278 | buttonClick() { 279 | const soft = this 280 | this.bjt.visible = true 281 | 282 | for (let i = 0; i < soft.animalArray.length; i++) { 283 | if (soft.kong.y - 10 < soft.animalArray[i].animal.y && soft.kong.y + 10 > soft.animalArray[i].animal.y) { 284 | this.soft.score += 10 285 | this.soft.scoreTxt.text = this.soft.score 286 | this.animalCeng.removeChild(soft.animalArray[i].animal) 287 | soft.animalArray.splice(i, 1) 288 | this.scoreAction("perfect") 289 | 290 | } else if (soft.kong.y - 20 < soft.animalArray[i].animal.y && soft.kong.y + 20 > soft.animalArray[i].animal.y) { 291 | this.soft.score += 5 292 | this.soft.scoreTxt.text = this.soft.score 293 | this.animalCeng.removeChild(soft.animalArray[i].animal) 294 | soft.animalArray.splice(i, 1) 295 | this.scoreAction("good") 296 | } else if (soft.kong.y - 30 < soft.animalArray[i].animal.y && soft.kong.y + 30 > soft.animalArray[i].animal.y) { 297 | this.soft.score += 1 298 | this.soft.scoreTxt.text = this.soft.score 299 | this.animalCeng.removeChild(soft.animalArray[i].animal) 300 | soft.animalArray.splice(i, 1) 301 | this.scoreAction("bad") 302 | } 303 | } 304 | } 305 | 306 | keyDown() { 307 | this.buttonClick() 308 | } 309 | 310 | keyUp() { 311 | this.bjt.visible = false 312 | } 313 | 314 | scoreAction(name) { 315 | let score = new PIXI.Sprite.fromImage(`${assetUrl}/` + name + ".png") 316 | this.gameObjectCeng.addChild(score) 317 | score.y = 540 318 | score.x = this.bjt.x 319 | score.anchor.set(0.5, 0.5) 320 | score.alpha = 1 321 | this.scoreArray.push(score) 322 | } 323 | 324 | scoreMove() { 325 | for (let i = 0; i < this.scoreArray.length; i++) { 326 | let score = this.scoreArray[i] 327 | score.alpha -= 0.01 328 | score.y -= 2 329 | } 330 | for (let i = this.scoreArray.length - 1; i >= 0; i--) { 331 | let score = this.scoreArray[i] 332 | if (score.y <= 400) { 333 | this.gameObjectCeng.removeChild(score) 334 | this.scoreArray.splice(i, 1) 335 | } 336 | } 337 | } 338 | } 339 | 340 | class Animal { 341 | constructor(type, animalX) { 342 | let number = 3 343 | let typeMap = { 344 | 1: "lan", 345 | 2: "lv", 346 | 3: "hong", 347 | 4: "huang" 348 | } 349 | this.animal = new PIXI.Sprite.fromImage(`${assetUrl}/${typeMap[type]}${number}.png`) 350 | this.animal.anchor.set(0.5, 0.5) 351 | this.animal.x = animalX 352 | this.animal.y = 0 353 | } 354 | 355 | move() { 356 | this.animal.y += 3.33 357 | } 358 | } -------------------------------------------------------------------------------- /docs/eplayer.js: -------------------------------------------------------------------------------- 1 | class Eplayer extends HTMLElement { 2 | constructor() { 3 | super() 4 | this.doms = {} 5 | this.src = this.getAttribute('src') 6 | this.type = this.getAttribute('type') 7 | this.cover = this.getAttribute('cover') 8 | this.live = JSON.parse(this.getAttribute('live')) 9 | this.danmaku = null 10 | this.subs = [] 11 | 12 | // 添加长按右箭头3倍速相关状态 13 | this.rightKeyHoldTimer = null 14 | this.rightKeyPressTime = null 15 | this.originalPlaybackRate = 1 16 | this.isRightKeyPressed = false 17 | this.isSpeedModeActive = false 18 | 19 | this.init() 20 | this.stream() 21 | } 22 | 23 | static get observedAttributes() { 24 | return ['src', 'type', 'danma', 'live','cover'] 25 | } 26 | 27 | attributeChangedCallback(name, oldVal, newVal) { 28 | if (name === 'src') { 29 | this.src = this.$('.video').src = newVal 30 | this.stream() 31 | this.$('.video').load() 32 | } 33 | if (name === 'type') { 34 | this.type = newVal 35 | this.stream() 36 | this.$('.video').load() 37 | } 38 | if (name === 'live') { 39 | this.live = JSON.parse(newVal) 40 | if (this.live) { 41 | this.$('.progress').style.display = 'none' 42 | this.$('.time').style.display = 'none' 43 | } else { 44 | this.$('.progress').style.display = 'block' 45 | this.$('.time').style.display = 'inline-block' 46 | } 47 | } 48 | if (name === 'danmu') { 49 | this.danmaku.add({ 50 | msg: newVal 51 | }) 52 | } 53 | if (name === 'cover') { 54 | this.cover = newVal 55 | this.$('.rotate-img').setAttribute('src', newVal) 56 | this.$('.cover').style.background=`url(${newVal || ''}) center/cover no-repeat #fff` 57 | } 58 | } 59 | 60 | $(key) { 61 | return this.doms[key] 62 | } 63 | 64 | waiting() { 65 | this.$('.mark').removeEventListener('click', this.mark.bind(this)) 66 | this.$('.mark').classList.remove('playing') 67 | this.$('.mark').classList.add('loading') 68 | } 69 | 70 | stream() { 71 | switch (this.type) { 72 | case 'hls': 73 | if (Hls.isSupported()) { 74 | let hls = new Hls() 75 | hls.loadSource(this.src) 76 | hls.attachMedia(this.video) 77 | } 78 | break 79 | } 80 | } 81 | 82 | mark() { 83 | clearTimeout(this.timer) 84 | this.timer = setTimeout(() => this.play(), 200) 85 | } 86 | 87 | canplay() { 88 | this.$('.mark').classList.remove('loading') 89 | this.$('.mark').classList.add('playing') 90 | this.$('.mark').addEventListener('click', this.mark.bind(this)) 91 | this.$('.total').innerHTML = getTimeStr(this.video.duration) 92 | } 93 | 94 | play() { 95 | console.log(this.$('.img-container')) 96 | if (this.video.paused) { 97 | this.video.play() 98 | this.danmaku.resume() 99 | this.$('.img-container').classList.add('is-playing') 100 | this.$('.is-play').setAttribute('icon-id', 'pause') 101 | this.emit('play') 102 | } else { 103 | this.video.pause() 104 | this.danmaku.pause() 105 | this.$('.img-container').classList.remove('is-playing') 106 | this.$('.is-play').setAttribute('icon-id', 'play') 107 | this.emit('pause') 108 | } 109 | } 110 | 111 | volume() { 112 | if (this.video.muted) { 113 | this.video.muted = false 114 | this.$('.is-volume').setAttribute('icon-id', 'volume-ok') 115 | } else { 116 | this.video.muted = true 117 | this.$('.is-volume').setAttribute('icon-id', 'volume-x') 118 | } 119 | } 120 | 121 | update() { 122 | if (this.moving) return 123 | let cTime = getTimeStr(this.video.currentTime) 124 | if (this.video.buffered.length) { 125 | let bufferEnd = this.video.buffered.end(this.video.buffered.length - 1) 126 | this.$('.buffer').style.width = (bufferEnd / this.video.duration) * 100 + '%' 127 | } 128 | let offset = (this.video.currentTime / this.video.duration) * 100 129 | this.$('.now').innerHTML = cTime 130 | this.$('.current').style.width = offset + '%' 131 | } 132 | 133 | progress(e) { 134 | const progressBarRect = this.$('.progress').getBoundingClientRect() 135 | let clickX = e.clientX - progressBarRect.left 136 | clickX = Math.max(0, Math.min(clickX, progressBarRect.width)) 137 | const offsetRatio = clickX / progressBarRect.width 138 | 139 | this.video.currentTime = this.video.duration * offsetRatio 140 | this.$('.now').innerHTML = getTimeStr(this.video.currentTime) 141 | this.$('.current').style.width = offsetRatio * 100 + '%' 142 | } 143 | 144 | down(e) { 145 | e.preventDefault() 146 | this.moving = true 147 | const progressBarRect = this.$('.progress').getBoundingClientRect() 148 | this.progressBarWidth = progressBarRect.width 149 | this.initialClientX = e.clientX 150 | const initialOffsetRatio = this.video.currentTime / this.video.duration 151 | this.initialOffsetPixels = initialOffsetRatio * this.progressBarWidth 152 | 153 | document.addEventListener('pointermove', this.handleMove) 154 | document.addEventListener('pointerup', this.handleUp, { once: true }) 155 | } 156 | 157 | handleMove = (e) => { 158 | if (!this.moving) return 159 | 160 | const deltaX = e.clientX - this.initialClientX 161 | let newOffsetPixels = this.initialOffsetPixels + deltaX 162 | newOffsetPixels = Math.max(0, Math.min(newOffsetPixels, this.progressBarWidth)) 163 | 164 | this.$('.current').style.width = (newOffsetPixels / this.progressBarWidth) * 100 + '%' 165 | 166 | const newTime = (newOffsetPixels / this.progressBarWidth) * this.video.duration 167 | this.video.currentTime = Math.max(0, Math.min(newTime, this.video.duration)) 168 | 169 | this.$('.now').innerHTML = getTimeStr(this.video.currentTime) 170 | } 171 | 172 | handleUp = (e) => { 173 | this.moving = false 174 | document.removeEventListener('pointermove', this.handleMove) 175 | delete this.progressBarWidth 176 | delete this.initialClientX 177 | delete this.initialOffsetPixels 178 | } 179 | 180 | move(e) { 181 | let offset = e.clientX - this.disX + 12 182 | if (offset < 0) offset = 0 183 | if (offset > this.$('.progress').clientWidth) { 184 | offset = this.$('.progress').clientWidth 185 | } 186 | this.$('.current').style.width = offset + 'px' 187 | this.video.currentTime = (offset / this.$('.progress').clientWidth) * this.video.duration 188 | this.$('.now').innerHTML = getTimeStr(this.video.currentTime) 189 | } 190 | 191 | alow() { 192 | clearTimeout(this.timer) 193 | this.$('.mark').style.cursor = 'default' 194 | this.$('.eplayer').classList.add('hover') 195 | if (!this.cover) { 196 | this.timer = setTimeout(() => { 197 | this.$('.eplayer').classList.remove('hover') 198 | }, 5000) 199 | } 200 | } 201 | 202 | keydown(e) { 203 | e.preventDefault() 204 | switch (e.keyCode) { 205 | case 37: // 左箭头 - 后退10秒 206 | this.video.currentTime = Math.max(0, this.video.currentTime - 10) 207 | break 208 | case 39: // 右箭头 - 前进10秒 (支持长按3倍速) 209 | if (!this.isRightKeyPressed) { 210 | this.isRightKeyPressed = true 211 | this.rightKeyPressTime = Date.now() 212 | this.originalPlaybackRate = this.video.playbackRate 213 | 214 | // 设置定时器,如果持续按住超过500ms则开启3倍速 215 | this.rightKeyHoldTimer = setTimeout(() => { 216 | this.isSpeedModeActive = true 217 | this.video.playbackRate = 3 218 | this.$('.speed').innerText = '3x' 219 | this.$('.speed-indicator').style.display = 'block' 220 | }, 500) 221 | } 222 | break 223 | case 38: // 上箭头 - 增加音量5% 224 | e.preventDefault() 225 | this.video.volume = Math.min(1, this.video.volume + 0.05) 226 | break 227 | case 40: // 下箭头 - 减少音量5% 228 | e.preventDefault() 229 | this.video.volume = Math.max(0, this.video.volume - 0.05) 230 | break 231 | case 32: // 空格键 - 播放/暂停 232 | e.preventDefault() 233 | this.play() 234 | break 235 | case 77: // M键 - 静音/取消静音 236 | this.volume() 237 | break 238 | default: 239 | } 240 | } 241 | 242 | keyup(e) { 243 | switch (e.keyCode) { 244 | case 39: // 右箭头松开 245 | if (this.isRightKeyPressed) { 246 | const pressDuration = Date.now() - this.rightKeyPressTime 247 | this.isRightKeyPressed = false 248 | 249 | // 清除定时器 250 | if (this.rightKeyHoldTimer) { 251 | clearTimeout(this.rightKeyHoldTimer) 252 | this.rightKeyHoldTimer = null 253 | } 254 | 255 | // 判断是短按还是长按 256 | if (pressDuration < 500 && !this.isSpeedModeActive) { 257 | // 短按:只快进10秒 258 | this.video.currentTime = Math.min(this.video.duration, this.video.currentTime + 10) 259 | } else if (this.isSpeedModeActive) { 260 | // 长按结束:恢复原播放速度并隐藏提示 261 | this.video.playbackRate = this.originalPlaybackRate 262 | this.$('.speed').innerText = this.originalPlaybackRate + 'x' 263 | this.$('.speed-indicator').style.display = 'none' 264 | this.isSpeedModeActive = false 265 | } 266 | 267 | // 重置状态 268 | this.rightKeyPressTime = null 269 | } 270 | break 271 | default: 272 | } 273 | } 274 | 275 | ended() { 276 | // this.$('.is-play').classList.replace('ep-pause', 'ep-play') 277 | } 278 | 279 | full() { 280 | if (isFullScreen()) { 281 | if (document.exitFullscreen) { 282 | document.exitFullscreen() 283 | } else if (document.mozCancelFullScreen) { 284 | document.mozCancelFullScreen() 285 | } else if (document.webkitCancelFullScreen) { 286 | document.webkitCancelFullScreen() 287 | } else if (document.msExitFullscreen) { 288 | document.msExitFullscreen() 289 | } 290 | 291 | screen.orientation.lock("portrait-primary") 292 | } else { 293 | let el = this.$('.eplayer') 294 | let rfs = el.requestFullScreen || el.webkitRequestFullScreen || el.mozRequestFullScreen || el.msRequestFullscreen 295 | rfs.call(el) 296 | screen.orientation.lock("landscape-primary") 297 | } 298 | } 299 | 300 | panel(e) { 301 | e.preventDefault() 302 | const panel = this.$('.panel') 303 | const eplayer = this.$('.eplayer') 304 | if (e.button !== 2) { 305 | panel.style.display = 'none' 306 | } else { 307 | panel.style.display = 'block' 308 | panel.style.height = panel.childElementCount * 24 + 'px' 309 | if (panel.offsetHeight + e.offsetY + 40 > eplayer.offsetHeight) { 310 | panel.style.top = '' 311 | panel.style.bottom = ((eplayer.offsetHeight - e.offsetY) / eplayer.offsetHeight) * 100 + '%' 312 | } else { 313 | panel.style.bottom = '' 314 | panel.style.top = (e.offsetY / eplayer.offsetHeight) * 100 + '%' 315 | } 316 | if (panel.offsetWidth + e.offsetX + 10 > eplayer.offsetWidth) { 317 | panel.style.left = '' 318 | panel.style.right = ((eplayer.offsetWidth - e.offsetX) / eplayer.offsetWidth) * 100 + '%' 319 | } else { 320 | panel.style.right = '' 321 | panel.style.left = (e.offsetX / eplayer.offsetWidth) * 100 + '%' 322 | } 323 | } 324 | } 325 | 326 | speed(e) { 327 | this.video.playbackRate === 3 ? (this.video.playbackRate = 1) : (this.video.playbackRate = this.video.playbackRate + 0.25) 328 | this.$('.speed').innerText = this.video.playbackRate + 'x' 329 | } 330 | 331 | init() { 332 | console.log(this.cover) 333 | let html = ` 334 | 610 | 611 |
612 |
613 | 614 |
615 |
616 |
617 |
倍速中
618 |
619 |
620 | 621 | 622 |
623 |
624 |
625 |
626 |
627 |
628 | 629 | 630 | 00:00/00:00 631 | 632 |
633 |
634 | 1x 635 | ${this.cover ? `画中画`:''} 636 | 637 | ${this.cover ? ``:''} 638 | ${this.cover ?``:""} 639 |
640 |
641 |
642 |
643 |
644 | ` 645 | let template = document.createElement('template') 646 | template.innerHTML = html 647 | this.attachShadow({ 648 | mode: 'open', 649 | }).appendChild(template.content.cloneNode(true)) 650 | 651 | const doms = [ 652 | '.img-container', 653 | '.video', 654 | '.mark', 655 | '.playing', 656 | '.loading', 657 | '.total', 658 | '.now', 659 | '.time', 660 | '.current', 661 | '.buffer', 662 | '.is-play', 663 | '.is-volume', 664 | '.dot', 665 | '.progress', 666 | '.controls', 667 | '.line', 668 | '.bg', 669 | '.eplayer', 670 | '.fullscreen', 671 | '.panel', 672 | '.speed', 673 | '.pip', 674 | '.danmaku', 675 | '.speed-indicator', 676 | '.rotate-img', 677 | '.cover' 678 | ] 679 | 680 | for (const key of doms) { 681 | let dom = this.shadowRoot.querySelectorAll(key) 682 | this.doms[key] = dom.length > 1 ? [...dom] : dom[0] 683 | } 684 | } 685 | 686 | connectedCallback() { 687 | this.video = this.$('.video') 688 | this.video.volume = 0.5 689 | this.danmaku = new Danmaku({ 690 | container: this.$('.danmaku') 691 | }) 692 | // setVolume(this.video.volume * 10, this.$('.line')) 693 | this.video.onwaiting = this.waiting.bind(this) 694 | this.video.oncanplay = this.canplay.bind(this) 695 | this.video.ontimeupdate = this.update.bind(this) 696 | this.video.onended = this.ended.bind(this) 697 | this.delegate('click', { 698 | '.is-volume': this.volume, 699 | '.fullscreen': this.full, 700 | '.is-play': this.play, 701 | '.ep-speed': this.speed, 702 | '.speed': this.speed, 703 | '.bg': this.progress, 704 | '.buffer': this.progress, 705 | '.current': this.progress, 706 | '.pip': this.pip, 707 | }) 708 | this.delegate('pointerdown', { 709 | '.dot': this.down, 710 | '.mark': this.panel, 711 | }) 712 | this.delegate('dblclick', { 713 | '.mark': (e) => { 714 | clearTimeout(this.timer) 715 | this.full() 716 | }, 717 | }) 718 | 719 | // 使用全局键盘监听以确保按键事件能在任何地方被捕获 720 | this.keydownHandler = this.keydown.bind(this) 721 | this.keyupHandler = this.keyup.bind(this) 722 | document.addEventListener('keydown', this.keydownHandler) 723 | document.addEventListener('keyup', this.keyupHandler) 724 | 725 | this.delegate('mousemove', this.alow) 726 | } 727 | 728 | disconnectedCallback() { 729 | // 清理全局键盘事件监听器 730 | if (this.keydownHandler) { 731 | document.removeEventListener('keydown', this.keydownHandler) 732 | } 733 | if (this.keyupHandler) { 734 | document.removeEventListener('keyup', this.keyupHandler) 735 | } 736 | 737 | // 清理长按定时器和状态 738 | if (this.rightKeyHoldTimer) { 739 | clearTimeout(this.rightKeyHoldTimer) 740 | this.rightKeyHoldTimer = null 741 | } 742 | this.isRightKeyPressed = false 743 | this.isSpeedModeActive = false 744 | this.rightKeyPressTime = null 745 | } 746 | 747 | delegate(type, map) { 748 | const that = this 749 | if (typeof map === 'function') { 750 | this.shadowRoot.addEventListener(type, map.bind(that)) 751 | } else { 752 | this.shadowRoot.addEventListener(type, (e) => { 753 | for (const key in map) e.target.matches(key) && map[key].call(that, e) 754 | }) 755 | } 756 | } 757 | 758 | pip(e) { 759 | if (!document.pictureInPictureElement) { 760 | this.video.requestPictureInPicture() 761 | } else { 762 | document.exitPictureInPicture() 763 | } 764 | } 765 | 766 | emit(name) { 767 | const fn = Eplayer.subs[name] 768 | fn && fn.call(this, this.shadowRoot) 769 | } 770 | 771 | } 772 | 773 | Eplayer.subs = {} 774 | 775 | Eplayer.use = function (name, cb) { 776 | this.subs[name] = cb 777 | } 778 | 779 | 780 | function getTimeStr(time) { 781 | let h = Math.floor(time / 3600) 782 | let m = Math.floor((time % 3600) / 60) 783 | let s = Math.floor(time % 60) 784 | h = h >= 10 ? h : '0' + h 785 | m = m >= 10 ? m : '0' + m 786 | s = s >= 10 ? s : '0' + s 787 | return h === '00' ? m + ':' + s : h + ':' + m + ':' + s 788 | } 789 | 790 | function isFullScreen() { 791 | return document.isFullScreen || document.webkitIsFullScreen || document.mozIsFullScreen 792 | } 793 | 794 | ; (function () { 795 | let link = document.createElement('script') 796 | link.setAttribute('src', 'https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/icons_34101_11.6161dfd06f46009a9dea0fcffc6234bf.js') 797 | document.head.appendChild(link) 798 | })() 799 | 800 | customElements.define('e-player', Eplayer) 801 | --------------------------------------------------------------------------------