diff options
author | Chocobozzz <me@florianbigard.com> | 2023-06-01 14:51:16 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-06-29 10:16:55 +0200 |
commit | d8f39b126d9fe4bec1c12fb213548cc6edc87867 (patch) | |
tree | 7f0f1cb23165cf4dd789b2d78b1fef7ee116f647 /client/src/assets/player/shared | |
parent | 1fb7d094229acdc190c3f7551b43ac5445814dee (diff) | |
download | PeerTube-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.ts | 1 | ||||
-rw-r--r-- | client/src/assets/player/shared/control-bar/storyboard-plugin.ts | 184 |
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' | |||
3 | export * from './peertube-link-button' | 3 | export * from './peertube-link-button' |
4 | export * from './peertube-live-display' | 4 | export * from './peertube-live-display' |
5 | export * from './peertube-load-progress-bar' | 5 | export * from './peertube-load-progress-bar' |
6 | export * from './storyboard-plugin' | ||
6 | export * from './theater-button' | 7 | export * 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 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { 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 | |||
7 | const Plugin = videojs.getPlugin('plugin') | ||
8 | |||
9 | class 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 | |||
182 | videojs.registerPlugin('storyboard', StoryboardPlugin) | ||
183 | |||
184 | export { StoryboardPlugin } | ||