├── .gitignore
├── README.md
├── docs
├── CNAME
├── danmaku.js
├── eplayer.js
├── game.html
├── icons
│ ├── danmu-ok.svg
│ ├── danmu-op.svg
│ ├── danmu-x.svg
│ ├── emoj.svg
│ ├── fullscreen.svg
│ ├── later.svg
│ ├── pause.svg
│ ├── play.svg
│ ├── volume-ok.svg
│ ├── volume-x.svg
│ └── web-fullscreen.svg
├── index.html
└── mug.js
└── package.json
/.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/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 | # eplayer [](https://npmjs.com/package/eplayer) [](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 | 
200 |
201 |
--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | eplayer.js.org
--------------------------------------------------------------------------------
/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/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.live = JSON.parse(this.getAttribute('live'))
8 | this.danmaku = null
9 | this.subs = []
10 |
11 | this.init()
12 | this.stream()
13 | }
14 |
15 | static get observedAttributes() {
16 | return ['src', 'type', 'danma', 'live']
17 | }
18 |
19 | attributeChangedCallback(name, oldVal, newVal) {
20 | if (name === 'src') {
21 | this.src = this.$('.video').src = newVal
22 | this.stream()
23 | this.$('.video').load()
24 | }
25 | if (name === 'type') {
26 | this.type = newVal
27 | this.stream()
28 | this.$('.video').load()
29 | }
30 | if (name === 'live') {
31 | this.live = JSON.parse(newVal)
32 | if (this.live) {
33 | this.$('.progress').style.display = 'none'
34 | this.$('.time').style.display = 'none'
35 | } else {
36 | this.$('.progress').style.display = 'block'
37 | this.$('.time').style.display = 'inline-block'
38 | }
39 | }
40 | if (name === 'danma') {
41 | this.danmaku.add({
42 | msg: newVal
43 | })
44 | }
45 | }
46 |
47 | $(key) {
48 | return this.doms[key]
49 | }
50 |
51 | waiting() {
52 | this.$('.mark').removeEventListener('click', this.mark.bind(this))
53 | this.$('.mark').classList.remove('playing')
54 | this.$('.mark').classList.add('loading')
55 | }
56 |
57 | stream() {
58 | switch (this.type) {
59 | case 'hls':
60 | if (Hls.isSupported()) {
61 | let hls = new Hls()
62 | hls.loadSource(this.src)
63 | hls.attachMedia(this.video)
64 | }
65 | break
66 | }
67 | }
68 |
69 | mark() {
70 | clearTimeout(this.timer)
71 | this.timer = setTimeout(() => this.play(), 200)
72 | }
73 |
74 | canplay() {
75 | this.$('.mark').classList.remove('loading')
76 | this.$('.mark').classList.add('playing')
77 | this.$('.mark').addEventListener('click', this.mark.bind(this))
78 | this.$('.total').innerHTML = getTimeStr(this.video.duration)
79 | }
80 |
81 | play() {
82 | if (this.video.paused) {
83 | this.video.play()
84 | this.danmaku.resume()
85 | this.$('.is-play').setAttribute('icon-id', 'pause')
86 | this.emit('play')
87 | } else {
88 | this.video.pause()
89 | this.danmaku.pause()
90 | this.$('.is-play').setAttribute('icon-id', 'play')
91 | this.emit('pause')
92 | }
93 | }
94 |
95 | volume() {
96 | if (this.video.muted) {
97 | this.video.muted = false
98 | this.$('.is-volume').setAttribute('icon-id', 'volume-ok')
99 | } else {
100 | this.video.muted = true
101 | this.$('.is-volume').setAttribute('icon-id', 'volume-x')
102 | }
103 | }
104 |
105 | update() {
106 | if (this.moving) return
107 | let cTime = getTimeStr(this.video.currentTime)
108 | if (this.video.buffered.length) {
109 | let bufferEnd = this.video.buffered.end(this.video.buffered.length - 1)
110 | this.$('.buffer').style.width = (bufferEnd / this.video.duration) * 100 + '%'
111 | }
112 | let offset = (this.video.currentTime / this.video.duration) * 100
113 | this.$('.now').innerHTML = cTime
114 | this.$('.current').style.width = offset + '%'
115 | }
116 |
117 | progress(e) {
118 | const progressBarRect = this.$('.progress').getBoundingClientRect()
119 | let clickX = e.clientX - progressBarRect.left
120 | clickX = Math.max(0, Math.min(clickX, progressBarRect.width))
121 | const offsetRatio = clickX / progressBarRect.width
122 |
123 | this.video.currentTime = this.video.duration * offsetRatio
124 | this.$('.now').innerHTML = getTimeStr(this.video.currentTime)
125 | this.$('.current').style.width = offsetRatio * 100 + '%'
126 | }
127 |
128 | down(e) {
129 | e.preventDefault()
130 | this.moving = true
131 | const progressBarRect = this.$('.progress').getBoundingClientRect()
132 | this.progressBarWidth = progressBarRect.width
133 | this.initialClientX = e.clientX
134 | const initialOffsetRatio = this.video.currentTime / this.video.duration
135 | this.initialOffsetPixels = initialOffsetRatio * this.progressBarWidth
136 |
137 | document.addEventListener('pointermove', this.handleMove)
138 | document.addEventListener('pointerup', this.handleUp, { once: true })
139 | }
140 |
141 | handleMove = (e) => {
142 | if (!this.moving) return
143 |
144 | const deltaX = e.clientX - this.initialClientX
145 | let newOffsetPixels = this.initialOffsetPixels + deltaX
146 | newOffsetPixels = Math.max(0, Math.min(newOffsetPixels, this.progressBarWidth))
147 |
148 | this.$('.current').style.width = (newOffsetPixels / this.progressBarWidth) * 100 + '%'
149 |
150 | const newTime = (newOffsetPixels / this.progressBarWidth) * this.video.duration
151 | this.video.currentTime = Math.max(0, Math.min(newTime, this.video.duration))
152 |
153 | this.$('.now').innerHTML = getTimeStr(this.video.currentTime)
154 | }
155 |
156 | handleUp = (e) => {
157 | this.moving = false
158 | document.removeEventListener('pointermove', this.handleMove)
159 | delete this.progressBarWidth
160 | delete this.initialClientX
161 | delete this.initialOffsetPixels
162 | }
163 |
164 | move(e) {
165 | let offset = e.clientX - this.disX + 12
166 | if (offset < 0) offset = 0
167 | if (offset > this.$('.progress').clientWidth) {
168 | offset = this.$('.progress').clientWidth
169 | }
170 | this.$('.current').style.width = offset + 'px'
171 | this.video.currentTime = (offset / this.$('.progress').clientWidth) * this.video.duration
172 | this.$('.now').innerHTML = getTimeStr(this.video.currentTime)
173 | }
174 |
175 | alow() {
176 | clearTimeout(this.timer)
177 | this.$('.mark').style.cursor = 'default'
178 | this.$('.eplayer').classList.add('hover')
179 | this.timer = setTimeout(() => {
180 | this.$('.eplayer').classList.remove('hover')
181 | }, 5000)
182 | }
183 |
184 | keydown(e) {
185 | switch (e.keyCode) {
186 | case 37:
187 | this.video.currentTime -= 10
188 | break
189 | case 39:
190 | this.video.currentTime += 10
191 | break
192 | case 38:
193 | try {
194 | this.video.volume = parseInt(this.video.volume * 100) / 100 + 0.05
195 | } catch (e) { }
196 | break
197 | case 40:
198 | try {
199 | this.video.volume = parseInt(this.video.volume * 100) / 100 - 0.05
200 | } catch (e) { }
201 | break
202 | case 32:
203 | this.play()
204 | break
205 | default:
206 | }
207 | }
208 |
209 | ended() {
210 | // this.$('.is-play').classList.replace('ep-pause', 'ep-play')
211 | }
212 |
213 | full() {
214 | if (isFullScreen()) {
215 | if (document.exitFullscreen) {
216 | document.exitFullscreen()
217 | } else if (document.mozCancelFullScreen) {
218 | document.mozCancelFullScreen()
219 | } else if (document.webkitCancelFullScreen) {
220 | document.webkitCancelFullScreen()
221 | } else if (document.msExitFullscreen) {
222 | document.msExitFullscreen()
223 | }
224 |
225 | screen.orientation.lock("portrait-primary")
226 | } else {
227 | let el = this.$('.eplayer')
228 | let rfs = el.requestFullScreen || el.webkitRequestFullScreen || el.mozRequestFullScreen || el.msRequestFullscreen
229 | rfs.call(el)
230 | screen.orientation.lock("landscape-primary")
231 | }
232 | }
233 |
234 | panel(e) {
235 | e.preventDefault()
236 | const panel = this.$('.panel')
237 | const eplayer = this.$('.eplayer')
238 | if (e.button !== 2) {
239 | panel.style.display = 'none'
240 | } else {
241 | panel.style.display = 'block'
242 | panel.style.height = panel.childElementCount * 24 + 'px'
243 | if (panel.offsetHeight + e.offsetY + 40 > eplayer.offsetHeight) {
244 | panel.style.top = ''
245 | panel.style.bottom = ((eplayer.offsetHeight - e.offsetY) / eplayer.offsetHeight) * 100 + '%'
246 | } else {
247 | panel.style.bottom = ''
248 | panel.style.top = (e.offsetY / eplayer.offsetHeight) * 100 + '%'
249 | }
250 | if (panel.offsetWidth + e.offsetX + 10 > eplayer.offsetWidth) {
251 | panel.style.left = ''
252 | panel.style.right = ((eplayer.offsetWidth - e.offsetX) / eplayer.offsetWidth) * 100 + '%'
253 | } else {
254 | panel.style.right = ''
255 | panel.style.left = (e.offsetX / eplayer.offsetWidth) * 100 + '%'
256 | }
257 | }
258 | }
259 |
260 | speed(e) {
261 | this.video.playbackRate === 3 ? (this.video.playbackRate = 1) : (this.video.playbackRate = this.video.playbackRate + 0.25)
262 | this.$('.speed').innerText = this.video.playbackRate + 'x'
263 | }
264 |
265 | init() {
266 | let html = `
267 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
463 |
464 |
465 |
466 |
467 | 00:00/00:00
468 |
469 |
470 |
471 | 倍速
472 | 画中画
473 |
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 | `
482 | let template = document.createElement('template')
483 | template.innerHTML = html
484 | this.attachShadow({
485 | mode: 'open',
486 | }).appendChild(template.content.cloneNode(true))
487 |
488 | const doms = [
489 | '.video',
490 | '.mark',
491 | '.playing',
492 | '.loading',
493 | '.total',
494 | '.now',
495 | '.time',
496 | '.current',
497 | '.buffer',
498 | '.is-play',
499 | '.is-volume',
500 | '.dot',
501 | '.progress',
502 | '.controls',
503 | '.line',
504 | '.bg',
505 | '.eplayer',
506 | '.fullscreen',
507 | '.panel',
508 | '.speed',
509 | '.pip',
510 | '.danmaku'
511 | ]
512 |
513 | for (const key of doms) {
514 | let dom = this.shadowRoot.querySelectorAll(key)
515 | this.doms[key] = dom.length > 1 ? [...dom] : dom[0]
516 | }
517 | }
518 |
519 | connectedCallback() {
520 | this.video = this.$('.video')
521 | this.video.volume = 0.5
522 | this.danmaku = new Danmaku({
523 | container: this.$('.danmaku')
524 | })
525 | // setVolume(this.video.volume * 10, this.$('.line'))
526 | this.video.onwaiting = this.waiting.bind(this)
527 | this.video.oncanplay = this.canplay.bind(this)
528 | this.video.ontimeupdate = this.update.bind(this)
529 | this.video.onended = this.ended.bind(this)
530 | this.delegate('click', {
531 | '.is-volume': this.volume,
532 | '.fullscreen': this.full,
533 | '.is-play': this.play,
534 | '.ep-speed': this.speed,
535 | '.speed': this.speed,
536 | '.bg': this.progress,
537 | '.buffer': this.progress,
538 | '.current': this.progress,
539 | '.pip': this.pip,
540 | })
541 | this.delegate('pointerdown', {
542 | '.dot': this.down,
543 | '.mark': this.panel,
544 | })
545 | this.delegate('dblclick', {
546 | '.mark': (e) => {
547 | clearTimeout(this.timer)
548 | this.full()
549 | },
550 | })
551 | this.delegate('keydown', this.keydown)
552 | this.delegate('mousemove', this.alow)
553 | }
554 |
555 | delegate(type, map) {
556 | const that = this
557 | if (typeof map === 'function') {
558 | this.shadowRoot.addEventListener(type, map.bind(that))
559 | } else {
560 | this.shadowRoot.addEventListener(type, (e) => {
561 | for (const key in map) e.target.matches(key) && map[key].call(that, e)
562 | })
563 | }
564 | }
565 |
566 | pip(e) {
567 | if (!document.pictureInPictureElement) {
568 | this.video.requestPictureInPicture()
569 | } else {
570 | document.exitPictureInPicture()
571 | }
572 | }
573 |
574 | emit (name) {
575 | const fn = Eplayer.subs[name]
576 | fn && fn.call(this, this.shadowRoot)
577 | }
578 |
579 | }
580 |
581 | Eplayer.subs = {}
582 |
583 | Eplayer.use = function (name, cb) {
584 | this.subs[name] = cb
585 | }
586 |
587 |
588 | function getTimeStr(time) {
589 | let h = Math.floor(time / 3600)
590 | let m = Math.floor((time % 3600) / 60)
591 | let s = Math.floor(time % 60)
592 | h = h >= 10 ? h : '0' + h
593 | m = m >= 10 ? m : '0' + m
594 | s = s >= 10 ? s : '0' + s
595 | return h === '00' ? m + ':' + s : h + ':' + m + ':' + s
596 | }
597 |
598 | function isFullScreen() {
599 | return document.isFullScreen || document.webkitIsFullScreen || document.mozIsFullScreen
600 | }
601 |
602 | ; (function () {
603 | let link = document.createElement('script')
604 | link.setAttribute('src', 'https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/icons_34101_11.6161dfd06f46009a9dea0fcffc6234bf.js')
605 | document.head.appendChild(link)
606 | })()
607 |
608 | customElements.define('e-player', Eplayer)
609 |
--------------------------------------------------------------------------------
/docs/game.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/docs/icons/danmu-ok.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/icons/danmu-op.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/icons/danmu-x.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/icons/emoj.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/icons/fullscreen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/icons/later.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/icons/pause.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/icons/play.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/icons/volume-ok.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/icons/volume-x.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/icons/web-fullscreen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | eplayer
9 |
10 |
11 |
12 |
13 |
14 |
61 |
62 |
63 |
65 |
66 |
67 |
68 |
69 |
70 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eplayer",
3 | "version": "1.6.6",
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 |
--------------------------------------------------------------------------------