aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/assets/player/shared
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-06-01 14:51:16 +0200
committerChocobozzz <me@florianbigard.com>2023-06-29 10:16:55 +0200
commitd8f39b126d9fe4bec1c12fb213548cc6edc87867 (patch)
tree7f0f1cb23165cf4dd789b2d78b1fef7ee116f647 /client/src/assets/player/shared
parent1fb7d094229acdc190c3f7551b43ac5445814dee (diff)
downloadPeerTube-d8f39b126d9fe4bec1c12fb213548cc6edc87867.tar.gz
PeerTube-d8f39b126d9fe4bec1c12fb213548cc6edc87867.tar.zst
PeerTube-d8f39b126d9fe4bec1c12fb213548cc6edc87867.zip
Add storyboard support
Diffstat (limited to 'client/src/assets/player/shared')
-rw-r--r--client/src/assets/player/shared/control-bar/index.ts1
-rw-r--r--client/src/assets/player/shared/control-bar/storyboard-plugin.ts184
2 files changed, 185 insertions, 0 deletions
diff --git a/client/src/assets/player/shared/control-bar/index.ts b/client/src/assets/player/shared/control-bar/index.ts
index e71e90713..24877c267 100644
--- a/client/src/assets/player/shared/control-bar/index.ts
+++ b/client/src/assets/player/shared/control-bar/index.ts
@@ -3,4 +3,5 @@ export * from './p2p-info-button'
3export * from './peertube-link-button' 3export * from './peertube-link-button'
4export * from './peertube-live-display' 4export * from './peertube-live-display'
5export * from './peertube-load-progress-bar' 5export * from './peertube-load-progress-bar'
6export * from './storyboard-plugin'
6export * from './theater-button' 7export * from './theater-button'
diff --git a/client/src/assets/player/shared/control-bar/storyboard-plugin.ts b/client/src/assets/player/shared/control-bar/storyboard-plugin.ts
new file mode 100644
index 000000000..c1843f595
--- /dev/null
+++ b/client/src/assets/player/shared/control-bar/storyboard-plugin.ts
@@ -0,0 +1,184 @@
1import videojs from 'video.js'
2import { StoryboardOptions } from '../../types'
3
4// Big thanks to this beautiful plugin: https://github.com/phloxic/videojs-sprite-thumbnails
5// Adapted to respect peertube player style
6
7const Plugin = videojs.getPlugin('plugin')
8
9class StoryboardPlugin extends Plugin {
10 private url: string
11 private height: number
12 private width: number
13 private interval: number
14
15 private cached: boolean
16
17 private mouseTimeTooltip: videojs.MouseTimeDisplay
18 private seekBar: { el(): HTMLElement, mouseTimeDisplay: any, playProgressBar: any }
19 private progress: any
20
21 private spritePlaceholder: HTMLElement
22
23 private readonly sprites: { [id: string]: HTMLImageElement } = {}
24
25 private readonly boundedHijackMouseTooltip: typeof StoryboardPlugin.prototype.hijackMouseTooltip
26
27 constructor (player: videojs.Player, options: videojs.ComponentOptions & StoryboardOptions) {
28 super(player, options)
29
30 this.url = options.url
31 this.height = options.height
32 this.width = options.width
33 this.interval = options.interval
34
35 this.boundedHijackMouseTooltip = this.hijackMouseTooltip.bind(this)
36
37 this.player.ready(() => {
38 player.addClass('vjs-storyboard')
39
40 this.init()
41 })
42 }
43
44 init () {
45 const controls = this.player.controlBar as any
46
47 // default control bar component tree is expected
48 // https://docs.videojs.com/tutorial-components.html#default-component-tree
49 this.progress = controls?.progressControl
50 this.seekBar = this.progress?.seekBar
51
52 this.mouseTimeTooltip = this.seekBar?.mouseTimeDisplay?.timeTooltip
53
54 this.spritePlaceholder = videojs.dom.createEl('div', { className: 'vjs-storyboard-sprite-placeholder' }) as HTMLElement
55 this.seekBar?.el()?.appendChild(this.spritePlaceholder)
56
57 this.player.on([ 'ready', 'loadstart' ], evt => {
58 if (evt !== 'ready') {
59 const spriteSource = this.player.currentSources().find(source => {
60 return Object.prototype.hasOwnProperty.call(source, 'storyboard')
61 }) as any
62 const spriteOpts = spriteSource?.['storyboard'] as StoryboardOptions
63
64 if (spriteOpts) {
65 this.url = spriteOpts.url
66 this.height = spriteOpts.height
67 this.width = spriteOpts.width
68 this.interval = spriteOpts.interval
69 }
70 }
71
72 this.cached = !!this.sprites[this.url]
73
74 this.load()
75 })
76 }
77
78 private load () {
79 const spriteEvents = [ 'mousemove', 'touchmove' ]
80
81 if (this.isReady()) {
82 if (!this.cached) {
83 this.sprites[this.url] = videojs.dom.createEl('img', {
84 src: this.url
85 })
86 }
87 this.progress.on(spriteEvents, this.boundedHijackMouseTooltip)
88 } else {
89 this.progress.off(spriteEvents, this.boundedHijackMouseTooltip)
90
91 this.resetMouseTooltip()
92 }
93 }
94
95 private hijackMouseTooltip (evt: Event) {
96 const sprite = this.sprites[this.url]
97 const imgWidth = sprite.naturalWidth
98 const imgHeight = sprite.naturalHeight
99 const seekBarEl = this.seekBar.el()
100
101 if (!sprite.complete || !imgWidth || !imgHeight) {
102 this.resetMouseTooltip()
103 return
104 }
105
106 this.player.requestNamedAnimationFrame('StoryBoardPlugin#hijackMouseTooltip', () => {
107 const seekBarRect = videojs.dom.getBoundingClientRect(seekBarEl)
108 const playerRect = videojs.dom.getBoundingClientRect(this.player.el())
109
110 if (!seekBarRect || !playerRect) return
111
112 const seekBarX = videojs.dom.getPointerPosition(seekBarEl, evt).x
113 let position = seekBarX * this.player.duration()
114
115 const maxPosition = Math.round((imgHeight / this.height) * (imgWidth / this.width)) - 1
116 position = Math.min(position / this.interval, maxPosition)
117
118 const responsive = 600
119 const playerWidth = this.player.currentWidth()
120 const scaleFactor = responsive && playerWidth < responsive
121 ? playerWidth / responsive
122 : 1
123 const columns = imgWidth / this.width
124
125 const scaledWidth = this.width * scaleFactor
126 const scaledHeight = this.height * scaleFactor
127 const cleft = Math.floor(position % columns) * -scaledWidth
128 const ctop = Math.floor(position / columns) * -scaledHeight
129
130 const bgSize = `${imgWidth * scaleFactor}px ${imgHeight * scaleFactor}px`
131 const topOffset = -scaledHeight - 60
132
133 const previewHalfSize = Math.round(scaledWidth / 2)
134 let left = seekBarRect.width * seekBarX - previewHalfSize
135
136 // Seek bar doesn't take all the player width, so we can add/minus a few more pixels
137 const minLeft = playerRect.left - seekBarRect.left
138 const maxLeft = seekBarRect.width - scaledWidth + (playerRect.right - seekBarRect.right)
139
140 if (left < minLeft) left = minLeft
141 if (left > maxLeft) left = maxLeft
142
143 const tooltipStyle: { [id: string]: string } = {
144 'background-image': `url("${this.url}")`,
145 'background-repeat': 'no-repeat',
146 'background-position': `${cleft}px ${ctop}px`,
147 'background-size': bgSize,
148
149 'color': '#fff',
150 'text-shadow': '1px 1px #000',
151
152 'position': 'relative',
153
154 'top': `${topOffset}px`,
155
156 'border': '1px solid #000',
157
158 // border should not overlay thumbnail area
159 'width': `${scaledWidth + 2}px`,
160 'height': `${scaledHeight + 2}px`
161 }
162
163 tooltipStyle.left = `${left}px`
164
165 for (const [ key, value ] of Object.entries(tooltipStyle)) {
166 this.spritePlaceholder.style.setProperty(key, value)
167 }
168 })
169 }
170
171 private resetMouseTooltip () {
172 if (this.spritePlaceholder) {
173 this.spritePlaceholder.style.cssText = ''
174 }
175 }
176
177 private isReady () {
178 return this.mouseTimeTooltip && this.width && this.height && this.url
179 }
180}
181
182videojs.registerPlugin('storyboard', StoryboardPlugin)
183
184export { StoryboardPlugin }