diff options
Diffstat (limited to 'client/src/assets/player')
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' | |||
6 | import './shared/bezels/bezels-plugin' | 6 | import './shared/bezels/bezels-plugin' |
7 | import './shared/peertube/peertube-plugin' | 7 | import './shared/peertube/peertube-plugin' |
8 | import './shared/resolutions/peertube-resolutions-plugin' | 8 | import './shared/resolutions/peertube-resolutions-plugin' |
9 | import './shared/control-bar/storyboard-plugin' | ||
9 | import './shared/control-bar/next-previous-video-button' | 10 | import './shared/control-bar/next-previous-video-button' |
10 | import './shared/control-bar/p2p-info-button' | 11 | import './shared/control-bar/p2p-info-button' |
11 | import './shared/control-bar/peertube-link-button' | 12 | import './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) |
43 | CaptionsButton.prototype.label_ = ' ' | 44 | CaptionsButton.prototype.label_ = ' ' |
44 | 45 | ||
46 | // TODO: remove when https://github.com/videojs/video.js/pull/7598 is merged | ||
47 | const PlayProgressBar = videojs.getComponent('PlayProgressBar') as any | ||
48 | if (PlayProgressBar.prototype.options_.children.includes('timeTooltip') !== true) { | ||
49 | PlayProgressBar.prototype.options_.children.push('timeTooltip') | ||
50 | } | ||
51 | |||
45 | export class PeertubePlayerManager { | 52 | export 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' | |||
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 } | ||
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 @@ | |||
1 | import { PluginsManager } from '@root-helpers/plugins-manager' | 1 | import { PluginsManager } from '@root-helpers/plugins-manager' |
2 | import { LiveVideoLatencyMode, VideoFile } from '@shared/models' | 2 | import { LiveVideoLatencyMode, VideoFile } from '@shared/models' |
3 | import { PlaylistPluginOptions, VideoJSCaption } from './peertube-videojs-typings' | 3 | import { PlaylistPluginOptions, VideoJSCaption, VideoJSStoryboard } from './peertube-videojs-typings' |
4 | 4 | ||
5 | export type PlayerMode = 'webtorrent' | 'p2p-media-loader' | 5 | export 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 | ||
94 | type VideoJSStoryboard = { | ||
95 | url: string | ||
96 | width: number | ||
97 | height: number | ||
98 | interval: number | ||
99 | } | ||
100 | |||
92 | type PeerTubePluginOptions = { | 101 | type 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 | ||
130 | type StoryboardOptions = { | ||
131 | url: string | ||
132 | width: number | ||
133 | height: number | ||
134 | interval: number | ||
135 | } | ||
136 | |||
121 | type PlaylistPluginOptions = { | 137 | type PlaylistPluginOptions = { |
122 | elements: VideoPlaylistElement[] | 138 | elements: VideoPlaylistElement[] |
123 | 139 | ||
@@ -238,6 +254,7 @@ type PlaylistItemOptions = { | |||
238 | 254 | ||
239 | export { | 255 | export { |
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 | } |