aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/assets/player
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/assets/player')
-rw-r--r--client/src/assets/player/peertube-player-manager.ts11
-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
-rw-r--r--client/src/assets/player/types/manager-options.ts3
-rw-r--r--client/src/assets/player/types/peertube-videojs-typings.ts18
5 files changed, 216 insertions, 1 deletions
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
index 2781850b9..66d9c7298 100644
--- a/client/src/assets/player/peertube-player-manager.ts
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -6,6 +6,7 @@ import './shared/stats/stats-plugin'
6import './shared/bezels/bezels-plugin' 6import './shared/bezels/bezels-plugin'
7import './shared/peertube/peertube-plugin' 7import './shared/peertube/peertube-plugin'
8import './shared/resolutions/peertube-resolutions-plugin' 8import './shared/resolutions/peertube-resolutions-plugin'
9import './shared/control-bar/storyboard-plugin'
9import './shared/control-bar/next-previous-video-button' 10import './shared/control-bar/next-previous-video-button'
10import './shared/control-bar/p2p-info-button' 11import './shared/control-bar/p2p-info-button'
11import './shared/control-bar/peertube-link-button' 12import './shared/control-bar/peertube-link-button'
@@ -42,6 +43,12 @@ CaptionsButton.prototype.controlText_ = 'Subtitles/CC'
42// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) 43// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know)
43CaptionsButton.prototype.label_ = ' ' 44CaptionsButton.prototype.label_ = ' '
44 45
46// TODO: remove when https://github.com/videojs/video.js/pull/7598 is merged
47const PlayProgressBar = videojs.getComponent('PlayProgressBar') as any
48if (PlayProgressBar.prototype.options_.children.includes('timeTooltip') !== true) {
49 PlayProgressBar.prototype.options_.children.push('timeTooltip')
50}
51
45export class PeertubePlayerManager { 52export class PeertubePlayerManager {
46 private static playerElementClassName: string 53 private static playerElementClassName: string
47 private static playerElementAttributes: { name: string, value: string }[] = [] 54 private static playerElementAttributes: { name: string, value: string }[] = []
@@ -135,6 +142,10 @@ export class PeertubePlayerManager {
135 p2pEnabled: options.common.p2pEnabled 142 p2pEnabled: options.common.p2pEnabled
136 }) 143 })
137 144
145 if (options.common.storyboard) {
146 player.storyboard(options.common.storyboard)
147 }
148
138 player.on('p2pInfo', (_, data: PlayerNetworkInfo) => { 149 player.on('p2pInfo', (_, data: PlayerNetworkInfo) => {
139 if (data.source !== 'p2p-media-loader' || isNaN(data.bandwidthEstimate)) return 150 if (data.source !== 'p2p-media-loader' || isNaN(data.bandwidthEstimate)) return
140 151
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 }
diff --git a/client/src/assets/player/types/manager-options.ts b/client/src/assets/player/types/manager-options.ts
index 1f3a0aa2e..a73341b4c 100644
--- a/client/src/assets/player/types/manager-options.ts
+++ b/client/src/assets/player/types/manager-options.ts
@@ -1,6 +1,6 @@
1import { PluginsManager } from '@root-helpers/plugins-manager' 1import { PluginsManager } from '@root-helpers/plugins-manager'
2import { LiveVideoLatencyMode, VideoFile } from '@shared/models' 2import { LiveVideoLatencyMode, VideoFile } from '@shared/models'
3import { PlaylistPluginOptions, VideoJSCaption } from './peertube-videojs-typings' 3import { PlaylistPluginOptions, VideoJSCaption, VideoJSStoryboard } from './peertube-videojs-typings'
4 4
5export type PlayerMode = 'webtorrent' | 'p2p-media-loader' 5export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
6 6
@@ -78,6 +78,7 @@ export interface CommonOptions extends CustomizationOptions {
78 language?: string 78 language?: string
79 79
80 videoCaptions: VideoJSCaption[] 80 videoCaptions: VideoJSCaption[]
81 storyboard: VideoJSStoryboard
81 82
82 videoUUID: string 83 videoUUID: string
83 videoShortUUID: string 84 videoShortUUID: string
diff --git a/client/src/assets/player/types/peertube-videojs-typings.ts b/client/src/assets/player/types/peertube-videojs-typings.ts
index 723c42c5d..30d2b287f 100644
--- a/client/src/assets/player/types/peertube-videojs-typings.ts
+++ b/client/src/assets/player/types/peertube-videojs-typings.ts
@@ -49,6 +49,8 @@ declare module 'video.js' {
49 49
50 stats (options?: StatsCardOptions): StatsForNerdsPlugin 50 stats (options?: StatsCardOptions): StatsForNerdsPlugin
51 51
52 storyboard (options: StoryboardOptions): void
53
52 textTracks (): TextTrackList & { 54 textTracks (): TextTrackList & {
53 tracks_: (TextTrack & { id: string, label: string, src: string })[] 55 tracks_: (TextTrack & { id: string, label: string, src: string })[]
54 } 56 }
@@ -89,6 +91,13 @@ type VideoJSCaption = {
89 src: string 91 src: string
90} 92}
91 93
94type VideoJSStoryboard = {
95 url: string
96 width: number
97 height: number
98 interval: number
99}
100
92type PeerTubePluginOptions = { 101type PeerTubePluginOptions = {
93 mode: PlayerMode 102 mode: PlayerMode
94 103
@@ -118,6 +127,13 @@ type MetricsPluginOptions = {
118 videoUUID: string 127 videoUUID: string
119} 128}
120 129
130type StoryboardOptions = {
131 url: string
132 width: number
133 height: number
134 interval: number
135}
136
121type PlaylistPluginOptions = { 137type PlaylistPluginOptions = {
122 elements: VideoPlaylistElement[] 138 elements: VideoPlaylistElement[]
123 139
@@ -238,6 +254,7 @@ type PlaylistItemOptions = {
238 254
239export { 255export {
240 PlayerNetworkInfo, 256 PlayerNetworkInfo,
257 VideoJSStoryboard,
241 PlaylistItemOptions, 258 PlaylistItemOptions,
242 NextPreviousVideoButtonOptions, 259 NextPreviousVideoButtonOptions,
243 ResolutionUpdateData, 260 ResolutionUpdateData,
@@ -251,6 +268,7 @@ export {
251 PeerTubeResolution, 268 PeerTubeResolution,
252 VideoJSPluginOptions, 269 VideoJSPluginOptions,
253 LoadedQualityData, 270 LoadedQualityData,
271 StoryboardOptions,
254 PeerTubeLinkButtonOptions, 272 PeerTubeLinkButtonOptions,
255 PeerTubeP2PInfoButtonOptions 273 PeerTubeP2PInfoButtonOptions
256} 274}