diff options
Diffstat (limited to 'client/src/assets')
23 files changed, 1357 insertions, 624 deletions
diff --git a/client/src/assets/player/bezels/bezels-plugin.ts b/client/src/assets/player/bezels/bezels-plugin.ts index c2c251961..499177526 100644 --- a/client/src/assets/player/bezels/bezels-plugin.ts +++ b/client/src/assets/player/bezels/bezels-plugin.ts | |||
@@ -1,85 +1,12 @@ | |||
1 | // @ts-ignore | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | import * as videojs from 'video.js' | 2 | import './pause-bezel' |
3 | import { VideoJSComponentInterface } from '../peertube-videojs-typings' | ||
4 | 3 | ||
5 | function getPauseBezel () { | 4 | const Plugin = videojs.getPlugin('plugin') |
6 | return ` | ||
7 | <div class="vjs-bezels-pause"> | ||
8 | <div class="vjs-bezel" role="status" aria-label="Pause"> | ||
9 | <div class="vjs-bezel-icon"> | ||
10 | <svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%"> | ||
11 | <use class="vjs-svg-shadow" xlink:href="#vjs-id-1"></use> | ||
12 | <path class="vjs-svg-fill" d="M 12,26 16,26 16,10 12,10 z M 21,26 25,26 25,10 21,10 z" id="vjs-id-1"></path> | ||
13 | </svg> | ||
14 | </div> | ||
15 | </div> | ||
16 | </div> | ||
17 | ` | ||
18 | } | ||
19 | |||
20 | function getPlayBezel () { | ||
21 | return ` | ||
22 | <div class="vjs-bezels-play"> | ||
23 | <div class="vjs-bezel" role="status" aria-label="Play"> | ||
24 | <div class="vjs-bezel-icon"> | ||
25 | <svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%"> | ||
26 | <use class="vjs-svg-shadow" xlink:href="#vjs-id-2"></use> | ||
27 | <path class="vjs-svg-fill" d="M 12,26 18.5,22 18.5,14 12,10 z M 18.5,22 25,18 25,18 18.5,14 z" id="ytp-id-2"></path> | ||
28 | </svg> | ||
29 | </div> | ||
30 | </div> | ||
31 | </div> | ||
32 | ` | ||
33 | } | ||
34 | |||
35 | // @ts-ignore-start | ||
36 | const Component = videojs.getComponent('Component') | ||
37 | class PauseBezel extends Component { | ||
38 | options_: any | ||
39 | container: HTMLBodyElement | ||
40 | |||
41 | constructor (player: videojs.Player, options: any) { | ||
42 | super(player, options) | ||
43 | this.options_ = options | ||
44 | |||
45 | player.on('pause', (_: any) => { | ||
46 | if (player.seeking() || player.ended()) return | ||
47 | this.container.innerHTML = getPauseBezel() | ||
48 | this.showBezel() | ||
49 | }) | ||
50 | |||
51 | player.on('play', (_: any) => { | ||
52 | if (player.seeking()) return | ||
53 | this.container.innerHTML = getPlayBezel() | ||
54 | this.showBezel() | ||
55 | }) | ||
56 | } | ||
57 | 5 | ||
58 | createEl () { | ||
59 | const container = super.createEl('div', { | ||
60 | className: 'vjs-bezels-content' | ||
61 | }) | ||
62 | this.container = container | ||
63 | container.style.display = 'none' | ||
64 | |||
65 | return container | ||
66 | } | ||
67 | |||
68 | showBezel () { | ||
69 | this.container.style.display = 'inherit' | ||
70 | setTimeout(() => { | ||
71 | this.container.style.display = 'none' | ||
72 | }, 500) // matching the animation duration | ||
73 | } | ||
74 | } | ||
75 | // @ts-ignore-end | ||
76 | |||
77 | videojs.registerComponent('PauseBezel', PauseBezel) | ||
78 | |||
79 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | ||
80 | class BezelsPlugin extends Plugin { | 6 | class BezelsPlugin extends Plugin { |
81 | constructor (player: videojs.Player, options: any = {}) { | 7 | |
82 | super(player, options) | 8 | constructor (player: VideoJsPlayer, options?: videojs.ComponentOptions) { |
9 | super(player) | ||
83 | 10 | ||
84 | this.player.ready(() => { | 11 | this.player.ready(() => { |
85 | player.addClass('vjs-bezels') | 12 | player.addClass('vjs-bezels') |
@@ -90,4 +17,5 @@ class BezelsPlugin extends Plugin { | |||
90 | } | 17 | } |
91 | 18 | ||
92 | videojs.registerPlugin('bezels', BezelsPlugin) | 19 | videojs.registerPlugin('bezels', BezelsPlugin) |
20 | |||
93 | export { BezelsPlugin } | 21 | export { BezelsPlugin } |
diff --git a/client/src/assets/player/bezels/pause-bezel.ts b/client/src/assets/player/bezels/pause-bezel.ts new file mode 100644 index 000000000..98eb12099 --- /dev/null +++ b/client/src/assets/player/bezels/pause-bezel.ts | |||
@@ -0,0 +1,72 @@ | |||
1 | import videojs, { VideoJsPlayer } from 'video.js' | ||
2 | |||
3 | function getPauseBezel () { | ||
4 | return ` | ||
5 | <div class="vjs-bezels-pause"> | ||
6 | <div class="vjs-bezel" role="status" aria-label="Pause"> | ||
7 | <div class="vjs-bezel-icon"> | ||
8 | <svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%"> | ||
9 | <use class="vjs-svg-shadow" xlink:href="#vjs-id-1"></use> | ||
10 | <path class="vjs-svg-fill" d="M 12,26 16,26 16,10 12,10 z M 21,26 25,26 25,10 21,10 z" id="vjs-id-1"></path> | ||
11 | </svg> | ||
12 | </div> | ||
13 | </div> | ||
14 | </div> | ||
15 | ` | ||
16 | } | ||
17 | |||
18 | function getPlayBezel () { | ||
19 | return ` | ||
20 | <div class="vjs-bezels-play"> | ||
21 | <div class="vjs-bezel" role="status" aria-label="Play"> | ||
22 | <div class="vjs-bezel-icon"> | ||
23 | <svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%"> | ||
24 | <use class="vjs-svg-shadow" xlink:href="#vjs-id-2"></use> | ||
25 | <path class="vjs-svg-fill" d="M 12,26 18.5,22 18.5,14 12,10 z M 18.5,22 25,18 25,18 18.5,14 z" id="ytp-id-2"></path> | ||
26 | </svg> | ||
27 | </div> | ||
28 | </div> | ||
29 | </div> | ||
30 | ` | ||
31 | } | ||
32 | |||
33 | const Component = videojs.getComponent('Component') | ||
34 | class PauseBezel extends Component { | ||
35 | container: HTMLDivElement | ||
36 | |||
37 | constructor (player: VideoJsPlayer, options?: videojs.ComponentOptions) { | ||
38 | super(player, options) | ||
39 | |||
40 | player.on('pause', (_: any) => { | ||
41 | if (player.seeking() || player.ended()) return | ||
42 | this.container.innerHTML = getPauseBezel() | ||
43 | this.showBezel() | ||
44 | }) | ||
45 | |||
46 | player.on('play', (_: any) => { | ||
47 | if (player.seeking()) return | ||
48 | this.container.innerHTML = getPlayBezel() | ||
49 | this.showBezel() | ||
50 | }) | ||
51 | } | ||
52 | |||
53 | createEl () { | ||
54 | this.container = super.createEl('div', { | ||
55 | className: 'vjs-bezels-content' | ||
56 | }) as HTMLDivElement | ||
57 | |||
58 | this.container.style.display = 'none' | ||
59 | |||
60 | return this.container | ||
61 | } | ||
62 | |||
63 | showBezel () { | ||
64 | this.container.style.display = 'inherit' | ||
65 | |||
66 | setTimeout(() => { | ||
67 | this.container.style.display = 'none' | ||
68 | }, 500) // matching the animation duration | ||
69 | } | ||
70 | } | ||
71 | |||
72 | videojs.registerComponent('PauseBezel', PauseBezel) | ||
diff --git a/client/src/assets/player/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/p2p-media-loader/hls-plugin.ts new file mode 100644 index 000000000..d78e1ab90 --- /dev/null +++ b/client/src/assets/player/p2p-media-loader/hls-plugin.ts | |||
@@ -0,0 +1,626 @@ | |||
1 | // Thanks https://github.com/streamroot/videojs-hlsjs-plugin | ||
2 | // We duplicated this plugin to choose the hls.js version we want, because streamroot only provide a bundled file | ||
3 | |||
4 | import * as Hlsjs from 'hls.js' | ||
5 | import videojs, { VideoJsPlayer } from 'video.js' | ||
6 | import { HlsjsConfigHandlerOptions, QualityLevelRepresentation, QualityLevels, VideoJSTechHLS } from '../peertube-videojs-typings' | ||
7 | |||
8 | type ErrorCounts = { | ||
9 | [ type: string ]: number | ||
10 | } | ||
11 | |||
12 | type Metadata = { | ||
13 | levels: Hlsjs.Level[] | ||
14 | } | ||
15 | |||
16 | const registerSourceHandler = function (vjs: typeof videojs) { | ||
17 | if (!Hlsjs.isSupported()) { | ||
18 | console.warn('Hls.js is not supported in this browser!') | ||
19 | return | ||
20 | } | ||
21 | |||
22 | const html5 = vjs.getTech('Html5') | ||
23 | |||
24 | if (!html5) { | ||
25 | console.error('Not supported version if video.js') | ||
26 | return | ||
27 | } | ||
28 | |||
29 | // FIXME: typings | ||
30 | (html5 as any).registerSourceHandler({ | ||
31 | canHandleSource: function (source: videojs.Tech.SourceObject) { | ||
32 | const hlsTypeRE = /^application\/x-mpegURL|application\/vnd\.apple\.mpegurl$/i | ||
33 | const hlsExtRE = /\.m3u8/i | ||
34 | |||
35 | if (hlsTypeRE.test(source.type)) return 'probably' | ||
36 | if (hlsExtRE.test(source.src)) return 'maybe' | ||
37 | |||
38 | return '' | ||
39 | }, | ||
40 | |||
41 | handleSource: function (source: videojs.Tech.SourceObject, tech: VideoJSTechHLS) { | ||
42 | if (tech.hlsProvider) { | ||
43 | tech.hlsProvider.dispose() | ||
44 | } | ||
45 | |||
46 | tech.hlsProvider = new Html5Hlsjs(vjs, source, tech) | ||
47 | |||
48 | return tech.hlsProvider | ||
49 | } | ||
50 | }, 0); | ||
51 | |||
52 | // FIXME: typings | ||
53 | (vjs as any).Html5Hlsjs = Html5Hlsjs | ||
54 | } | ||
55 | |||
56 | function hlsjsConfigHandler (this: VideoJsPlayer, options: HlsjsConfigHandlerOptions) { | ||
57 | const player = this | ||
58 | |||
59 | if (!options) return | ||
60 | |||
61 | if (!player.srOptions_) { | ||
62 | player.srOptions_ = {} | ||
63 | } | ||
64 | |||
65 | if (!player.srOptions_.hlsjsConfig) { | ||
66 | player.srOptions_.hlsjsConfig = options.hlsjsConfig | ||
67 | } | ||
68 | |||
69 | if (!player.srOptions_.captionConfig) { | ||
70 | player.srOptions_.captionConfig = options.captionConfig | ||
71 | } | ||
72 | |||
73 | if (options.levelLabelHandler && !player.srOptions_.levelLabelHandler) { | ||
74 | player.srOptions_.levelLabelHandler = options.levelLabelHandler | ||
75 | } | ||
76 | } | ||
77 | |||
78 | const registerConfigPlugin = function (vjs: typeof videojs) { | ||
79 | // Used in Brightcove since we don't pass options directly there | ||
80 | const registerVjsPlugin = vjs.registerPlugin || vjs.plugin | ||
81 | registerVjsPlugin('hlsjs', hlsjsConfigHandler) | ||
82 | } | ||
83 | |||
84 | class Html5Hlsjs { | ||
85 | private static readonly hooks: { [id: string]: Function[] } = {} | ||
86 | |||
87 | private readonly videoElement: HTMLVideoElement | ||
88 | private readonly errorCounts: ErrorCounts = {} | ||
89 | private readonly player: VideoJsPlayer | ||
90 | private readonly tech: videojs.Tech | ||
91 | private readonly source: videojs.Tech.SourceObject | ||
92 | private readonly vjs: typeof videojs | ||
93 | |||
94 | private hls: Hlsjs & { manualLevel?: number } // FIXME: typings | ||
95 | private hlsjsConfig: Partial<Hlsjs.Config & { cueHandler: any }> = null | ||
96 | |||
97 | private _duration: number = null | ||
98 | private metadata: Metadata = null | ||
99 | private isLive: boolean = null | ||
100 | private dvrDuration: number = null | ||
101 | private edgeMargin: number = null | ||
102 | |||
103 | private handlers: { [ id in 'play' | 'addtrack' | 'playing' | 'textTracksChange' | 'audioTracksChange' ]: EventListener } = { | ||
104 | play: null, | ||
105 | addtrack: null, | ||
106 | playing: null, | ||
107 | textTracksChange: null, | ||
108 | audioTracksChange: null | ||
109 | } | ||
110 | |||
111 | private uiTextTrackHandled = false | ||
112 | |||
113 | constructor (vjs: typeof videojs, source: videojs.Tech.SourceObject, tech: videojs.Tech) { | ||
114 | this.vjs = vjs | ||
115 | this.source = source | ||
116 | |||
117 | this.tech = tech; | ||
118 | (this.tech as any).name_ = 'Hlsjs' | ||
119 | |||
120 | this.videoElement = tech.el() as HTMLVideoElement | ||
121 | this.player = vjs((tech.options_ as any).playerId) | ||
122 | |||
123 | this.videoElement.addEventListener('error', event => { | ||
124 | let errorTxt: string | ||
125 | const mediaError = (event.currentTarget as HTMLVideoElement).error | ||
126 | |||
127 | switch (mediaError.code) { | ||
128 | case mediaError.MEDIA_ERR_ABORTED: | ||
129 | errorTxt = 'You aborted the video playback' | ||
130 | break | ||
131 | case mediaError.MEDIA_ERR_DECODE: | ||
132 | errorTxt = 'The video playback was aborted due to a corruption problem or because the video used features your browser did not support' | ||
133 | this._handleMediaError(mediaError) | ||
134 | break | ||
135 | case mediaError.MEDIA_ERR_NETWORK: | ||
136 | errorTxt = 'A network error caused the video download to fail part-way' | ||
137 | break | ||
138 | case mediaError.MEDIA_ERR_SRC_NOT_SUPPORTED: | ||
139 | errorTxt = 'The video could not be loaded, either because the server or network failed or because the format is not supported' | ||
140 | break | ||
141 | |||
142 | default: | ||
143 | errorTxt = mediaError.message | ||
144 | } | ||
145 | |||
146 | console.error('MEDIA_ERROR: ', errorTxt) | ||
147 | }) | ||
148 | |||
149 | this.initialize() | ||
150 | } | ||
151 | |||
152 | duration () { | ||
153 | return this._duration || this.videoElement.duration || 0 | ||
154 | } | ||
155 | |||
156 | seekable () { | ||
157 | if (this.hls.media) { | ||
158 | if (!this.isLive) { | ||
159 | return this.vjs.createTimeRanges(0, this.hls.media.duration) | ||
160 | } | ||
161 | |||
162 | // Video.js doesn't seem to like floating point timeranges | ||
163 | const startTime = Math.round(this.hls.media.duration - this.dvrDuration) | ||
164 | const endTime = Math.round(this.hls.media.duration - this.edgeMargin) | ||
165 | |||
166 | return this.vjs.createTimeRanges(startTime, endTime) | ||
167 | } | ||
168 | |||
169 | return this.vjs.createTimeRanges() | ||
170 | } | ||
171 | |||
172 | // See comment for `initialize` method. | ||
173 | dispose () { | ||
174 | this.videoElement.removeEventListener('play', this.handlers.play) | ||
175 | this.videoElement.textTracks.removeEventListener('addtrack', this.handlers.addtrack) | ||
176 | this.videoElement.removeEventListener('playing', this.handlers.playing) | ||
177 | |||
178 | this.player.textTracks().removeEventListener('change', this.handlers.textTracksChange) | ||
179 | this.uiTextTrackHandled = false | ||
180 | |||
181 | this.player.audioTracks().removeEventListener('change', this.handlers.audioTracksChange) | ||
182 | |||
183 | this.hls.destroy() | ||
184 | } | ||
185 | |||
186 | static addHook (type: string, callback: Function) { | ||
187 | Html5Hlsjs.hooks[ type ] = this.hooks[ type ] || [] | ||
188 | Html5Hlsjs.hooks[ type ].push(callback) | ||
189 | } | ||
190 | |||
191 | static removeHook (type: string, callback: Function) { | ||
192 | if (Html5Hlsjs.hooks[ type ] === undefined) return false | ||
193 | |||
194 | const index = Html5Hlsjs.hooks[ type ].indexOf(callback) | ||
195 | if (index === -1) return false | ||
196 | |||
197 | Html5Hlsjs.hooks[ type ].splice(index, 1) | ||
198 | |||
199 | return true | ||
200 | } | ||
201 | |||
202 | private _executeHooksFor (type: string) { | ||
203 | if (Html5Hlsjs.hooks[ type ] === undefined) { | ||
204 | return | ||
205 | } | ||
206 | |||
207 | // ES3 and IE < 9 | ||
208 | for (let i = 0; i < Html5Hlsjs.hooks[ type ].length; i++) { | ||
209 | Html5Hlsjs.hooks[ type ][ i ](this.player, this.hls) | ||
210 | } | ||
211 | } | ||
212 | |||
213 | private _handleMediaError (error: any) { | ||
214 | if (this.errorCounts[ Hlsjs.ErrorTypes.MEDIA_ERROR ] === 1) { | ||
215 | console.info('trying to recover media error') | ||
216 | this.hls.recoverMediaError() | ||
217 | return | ||
218 | } | ||
219 | |||
220 | if (this.errorCounts[ Hlsjs.ErrorTypes.MEDIA_ERROR ] === 2) { | ||
221 | console.info('2nd try to recover media error (by swapping audio codec') | ||
222 | this.hls.swapAudioCodec() | ||
223 | this.hls.recoverMediaError() | ||
224 | return | ||
225 | } | ||
226 | |||
227 | if (this.errorCounts[ Hlsjs.ErrorTypes.MEDIA_ERROR ] > 2) { | ||
228 | console.info('bubbling media error up to VIDEOJS') | ||
229 | this.tech.error = () => error | ||
230 | this.tech.trigger('error') | ||
231 | return | ||
232 | } | ||
233 | } | ||
234 | |||
235 | private _onError (_event: any, data: Hlsjs.errorData) { | ||
236 | const error: { message: string, code?: number } = { | ||
237 | message: `HLS.js error: ${data.type} - fatal: ${data.fatal} - ${data.details}` | ||
238 | } | ||
239 | console.error(error.message) | ||
240 | |||
241 | // increment/set error count | ||
242 | if (this.errorCounts[ data.type ]) this.errorCounts[ data.type ] += 1 | ||
243 | else this.errorCounts[ data.type ] = 1 | ||
244 | |||
245 | // Implement simple error handling based on hls.js documentation | ||
246 | // https://github.com/dailymotion/hls.js/blob/master/API.md#fifth-step-error-handling | ||
247 | if (data.fatal) { | ||
248 | switch (data.type) { | ||
249 | case Hlsjs.ErrorTypes.NETWORK_ERROR: | ||
250 | console.info('bubbling network error up to VIDEOJS') | ||
251 | error.code = 2 | ||
252 | this.tech.error = () => error as any | ||
253 | this.tech.trigger('error') | ||
254 | break | ||
255 | |||
256 | case Hlsjs.ErrorTypes.MEDIA_ERROR: | ||
257 | error.code = 3 | ||
258 | this._handleMediaError(error) | ||
259 | break | ||
260 | |||
261 | default: | ||
262 | // cannot recover | ||
263 | this.hls.destroy() | ||
264 | console.info('bubbling error up to VIDEOJS') | ||
265 | this.tech.error = () => error as any | ||
266 | this.tech.trigger('error') | ||
267 | break | ||
268 | } | ||
269 | } | ||
270 | } | ||
271 | |||
272 | private switchQuality (qualityId: number) { | ||
273 | this.hls.nextLevel = qualityId | ||
274 | } | ||
275 | |||
276 | private _levelLabel (level: Hlsjs.Level) { | ||
277 | if (this.player.srOptions_.levelLabelHandler) { | ||
278 | return this.player.srOptions_.levelLabelHandler(level) | ||
279 | } | ||
280 | |||
281 | if (level.height) return level.height + 'p' | ||
282 | if (level.width) return Math.round(level.width * 9 / 16) + 'p' | ||
283 | if (level.bitrate) return (level.bitrate / 1000) + 'kbps' | ||
284 | |||
285 | return 0 | ||
286 | } | ||
287 | |||
288 | private _relayQualityChange (qualityLevels: QualityLevels) { | ||
289 | // Determine if it is "Auto" (all tracks enabled) | ||
290 | let isAuto = true | ||
291 | |||
292 | for (let i = 0; i < qualityLevels.length; i++) { | ||
293 | if (!qualityLevels[ i ]._enabled) { | ||
294 | isAuto = false | ||
295 | break | ||
296 | } | ||
297 | } | ||
298 | |||
299 | // Interact with ME | ||
300 | if (isAuto) { | ||
301 | this.hls.currentLevel = -1 | ||
302 | return | ||
303 | } | ||
304 | |||
305 | // Find ID of highest enabled track | ||
306 | let selectedTrack: number | ||
307 | |||
308 | for (selectedTrack = qualityLevels.length - 1; selectedTrack >= 0; selectedTrack--) { | ||
309 | if (qualityLevels[ selectedTrack ]._enabled) { | ||
310 | break | ||
311 | } | ||
312 | } | ||
313 | |||
314 | this.hls.currentLevel = selectedTrack | ||
315 | } | ||
316 | |||
317 | private _handleQualityLevels () { | ||
318 | if (!this.metadata) return | ||
319 | |||
320 | const qualityLevels = this.player.qualityLevels && this.player.qualityLevels() | ||
321 | if (!qualityLevels) return | ||
322 | |||
323 | for (let i = 0; i < this.metadata.levels.length; i++) { | ||
324 | const details = this.metadata.levels[ i ] | ||
325 | const representation: QualityLevelRepresentation = { | ||
326 | id: i, | ||
327 | width: details.width, | ||
328 | height: details.height, | ||
329 | bandwidth: details.bitrate, | ||
330 | bitrate: details.bitrate, | ||
331 | _enabled: true | ||
332 | } | ||
333 | |||
334 | const self = this | ||
335 | representation.enabled = function (this: QualityLevels, level: number, toggle?: boolean) { | ||
336 | // Brightcove switcher works TextTracks-style (enable tracks that it wants to ABR on) | ||
337 | if (typeof toggle === 'boolean') { | ||
338 | this[ level ]._enabled = toggle | ||
339 | self._relayQualityChange(this) | ||
340 | } | ||
341 | |||
342 | return this[ level ]._enabled | ||
343 | } | ||
344 | |||
345 | qualityLevels.addQualityLevel(representation) | ||
346 | } | ||
347 | } | ||
348 | |||
349 | private _notifyVideoQualities () { | ||
350 | if (!this.metadata) return | ||
351 | const cleanTracklist = [] | ||
352 | |||
353 | if (this.metadata.levels.length > 1) { | ||
354 | const autoLevel = { | ||
355 | id: -1, | ||
356 | label: 'auto', | ||
357 | selected: this.hls.manualLevel === -1 | ||
358 | } | ||
359 | cleanTracklist.push(autoLevel) | ||
360 | } | ||
361 | |||
362 | this.metadata.levels.forEach((level, index) => { | ||
363 | // Don't write in level (shared reference with Hls.js) | ||
364 | const quality = { | ||
365 | id: index, | ||
366 | selected: index === this.hls.manualLevel, | ||
367 | label: this._levelLabel(level) | ||
368 | } | ||
369 | |||
370 | cleanTracklist.push(quality) | ||
371 | }) | ||
372 | |||
373 | const payload = { | ||
374 | qualityData: { video: cleanTracklist }, | ||
375 | qualitySwitchCallback: this.switchQuality.bind(this) | ||
376 | } | ||
377 | |||
378 | this.tech.trigger('loadedqualitydata', payload) | ||
379 | |||
380 | // Self-de-register so we don't raise the payload multiple times | ||
381 | this.videoElement.removeEventListener('playing', this.handlers.playing) | ||
382 | } | ||
383 | |||
384 | private _updateSelectedAudioTrack () { | ||
385 | const playerAudioTracks = this.tech.audioTracks() | ||
386 | for (let j = 0; j < playerAudioTracks.length; j++) { | ||
387 | // FIXME: typings | ||
388 | if ((playerAudioTracks[ j ] as any).enabled) { | ||
389 | this.hls.audioTrack = j | ||
390 | break | ||
391 | } | ||
392 | } | ||
393 | } | ||
394 | |||
395 | private _onAudioTracks () { | ||
396 | const hlsAudioTracks = this.hls.audioTracks as (AudioTrack & { name?: string, lang?: string })[] // FIXME typings | ||
397 | const playerAudioTracks = this.tech.audioTracks() | ||
398 | |||
399 | if (hlsAudioTracks.length > 1 && playerAudioTracks.length === 0) { | ||
400 | // Add Hls.js audio tracks if not added yet | ||
401 | for (let i = 0; i < hlsAudioTracks.length; i++) { | ||
402 | playerAudioTracks.addTrack(new this.vjs.AudioTrack({ | ||
403 | id: i.toString(), | ||
404 | kind: 'alternative', | ||
405 | label: hlsAudioTracks[ i ].name || hlsAudioTracks[ i ].lang, | ||
406 | language: hlsAudioTracks[ i ].lang, | ||
407 | enabled: i === this.hls.audioTrack | ||
408 | })) | ||
409 | } | ||
410 | |||
411 | // Handle audio track change event | ||
412 | this.handlers.audioTracksChange = this._updateSelectedAudioTrack.bind(this) | ||
413 | playerAudioTracks.addEventListener('change', this.handlers.audioTracksChange) | ||
414 | } | ||
415 | } | ||
416 | |||
417 | private _getTextTrackLabel (textTrack: TextTrack) { | ||
418 | // Label here is readable label and is optional (used in the UI so if it is there it should be different) | ||
419 | return textTrack.label ? textTrack.label : textTrack.language | ||
420 | } | ||
421 | |||
422 | private _isSameTextTrack (track1: TextTrack, track2: TextTrack) { | ||
423 | return this._getTextTrackLabel(track1) === this._getTextTrackLabel(track2) | ||
424 | && track1.kind === track2.kind | ||
425 | } | ||
426 | |||
427 | private _updateSelectedTextTrack () { | ||
428 | const playerTextTracks = this.player.textTracks() | ||
429 | let activeTrack: TextTrack = null | ||
430 | |||
431 | for (let j = 0; j < playerTextTracks.length; j++) { | ||
432 | if (playerTextTracks[ j ].mode === 'showing') { | ||
433 | activeTrack = playerTextTracks[ j ] | ||
434 | break | ||
435 | } | ||
436 | } | ||
437 | |||
438 | const hlsjsTracks = this.videoElement.textTracks | ||
439 | for (let k = 0; k < hlsjsTracks.length; k++) { | ||
440 | if (hlsjsTracks[ k ].kind === 'subtitles' || hlsjsTracks[ k ].kind === 'captions') { | ||
441 | hlsjsTracks[ k ].mode = activeTrack && this._isSameTextTrack(hlsjsTracks[ k ], activeTrack) | ||
442 | ? 'showing' | ||
443 | : 'disabled' | ||
444 | } | ||
445 | } | ||
446 | } | ||
447 | |||
448 | private _startLoad () { | ||
449 | this.hls.startLoad(-1) | ||
450 | this.videoElement.removeEventListener('play', this.handlers.play) | ||
451 | } | ||
452 | |||
453 | private _oneLevelObjClone (obj: object) { | ||
454 | const result = {} | ||
455 | const objKeys = Object.keys(obj) | ||
456 | for (let i = 0; i < objKeys.length; i++) { | ||
457 | result[ objKeys[ i ] ] = obj[ objKeys[ i ] ] | ||
458 | } | ||
459 | |||
460 | return result | ||
461 | } | ||
462 | |||
463 | private _filterDisplayableTextTracks (textTracks: TextTrackList) { | ||
464 | const displayableTracks = [] | ||
465 | |||
466 | // Filter out tracks that is displayable (captions or subtitles) | ||
467 | for (let idx = 0; idx < textTracks.length; idx++) { | ||
468 | if (textTracks[ idx ].kind === 'subtitles' || textTracks[ idx ].kind === 'captions') { | ||
469 | displayableTracks.push(textTracks[ idx ]) | ||
470 | } | ||
471 | } | ||
472 | |||
473 | return displayableTracks | ||
474 | } | ||
475 | |||
476 | private _updateTextTrackList () { | ||
477 | const displayableTracks = this._filterDisplayableTextTracks(this.videoElement.textTracks) | ||
478 | const playerTextTracks = this.player.textTracks() | ||
479 | |||
480 | // Add stubs to make the caption switcher shows up | ||
481 | // Adding the Hls.js text track in will make us have double captions | ||
482 | for (let idx = 0; idx < displayableTracks.length; idx++) { | ||
483 | let isAdded = false | ||
484 | |||
485 | for (let jdx = 0; jdx < playerTextTracks.length; jdx++) { | ||
486 | if (this._isSameTextTrack(displayableTracks[ idx ], playerTextTracks[ jdx ])) { | ||
487 | isAdded = true | ||
488 | break | ||
489 | } | ||
490 | } | ||
491 | |||
492 | if (!isAdded) { | ||
493 | const hlsjsTextTrack = displayableTracks[ idx ] | ||
494 | this.player.addRemoteTextTrack({ | ||
495 | kind: hlsjsTextTrack.kind as videojs.TextTrack.Kind, | ||
496 | label: this._getTextTrackLabel(hlsjsTextTrack), | ||
497 | language: hlsjsTextTrack.language, | ||
498 | srclang: hlsjsTextTrack.language | ||
499 | }, false) | ||
500 | } | ||
501 | } | ||
502 | |||
503 | // Handle UI switching | ||
504 | this._updateSelectedTextTrack() | ||
505 | |||
506 | if (!this.uiTextTrackHandled) { | ||
507 | this.handlers.textTracksChange = this._updateSelectedTextTrack.bind(this) | ||
508 | playerTextTracks.addEventListener('change', this.handlers.textTracksChange) | ||
509 | |||
510 | this.uiTextTrackHandled = true | ||
511 | } | ||
512 | } | ||
513 | |||
514 | private _onMetaData (_event: any, data: Hlsjs.manifestLoadedData) { | ||
515 | // This could arrive before 'loadedqualitydata' handlers is registered, remember it so we can raise it later | ||
516 | this.metadata = data as any | ||
517 | this._handleQualityLevels() | ||
518 | } | ||
519 | |||
520 | private _createCueHandler (captionConfig: any) { | ||
521 | return { | ||
522 | newCue: (track: any, startTime: number, endTime: number, captionScreen: { rows: any[] }) => { | ||
523 | let row: any | ||
524 | let cue: VTTCue | ||
525 | let text: string | ||
526 | const VTTCue = (window as any).VTTCue || (window as any).TextTrackCue | ||
527 | |||
528 | for (let r = 0; r < captionScreen.rows.length; r++) { | ||
529 | row = captionScreen.rows[ r ] | ||
530 | text = '' | ||
531 | |||
532 | if (!row.isEmpty()) { | ||
533 | for (let c = 0; c < row.chars.length; c++) { | ||
534 | text += row.chars[ c ].ucharj | ||
535 | } | ||
536 | |||
537 | cue = new VTTCue(startTime, endTime, text.trim()) | ||
538 | |||
539 | // typeof null === 'object' | ||
540 | if (captionConfig != null && typeof captionConfig === 'object') { | ||
541 | // Copy client overridden property into the cue object | ||
542 | const configKeys = Object.keys(captionConfig) | ||
543 | |||
544 | for (let k = 0; k < configKeys.length; k++) { | ||
545 | cue[ configKeys[ k ] ] = captionConfig[ configKeys[ k ] ] | ||
546 | } | ||
547 | } | ||
548 | track.addCue(cue) | ||
549 | if (endTime === startTime) track.addCue(new VTTCue(endTime + 5, '')) | ||
550 | } | ||
551 | } | ||
552 | } | ||
553 | } | ||
554 | } | ||
555 | |||
556 | private _initHlsjs () { | ||
557 | const techOptions = this.tech.options_ as HlsjsConfigHandlerOptions | ||
558 | const srOptions_ = this.player.srOptions_ | ||
559 | |||
560 | const hlsjsConfigRef = srOptions_ && srOptions_.hlsjsConfig || techOptions.hlsjsConfig | ||
561 | // Hls.js will write to the reference thus change the object for later streams | ||
562 | this.hlsjsConfig = hlsjsConfigRef ? this._oneLevelObjClone(hlsjsConfigRef) : {} | ||
563 | |||
564 | if ([ '', 'auto' ].includes(this.videoElement.preload) && !this.videoElement.autoplay && this.hlsjsConfig.autoStartLoad === undefined) { | ||
565 | this.hlsjsConfig.autoStartLoad = false | ||
566 | } | ||
567 | |||
568 | const captionConfig = srOptions_ && srOptions_.captionConfig || techOptions.captionConfig | ||
569 | if (captionConfig) { | ||
570 | this.hlsjsConfig.cueHandler = this._createCueHandler(captionConfig) | ||
571 | } | ||
572 | |||
573 | // If the user explicitly sets autoStartLoad to false, we're not going to enter the if block above | ||
574 | // That's why we have a separate if block here to set the 'play' listener | ||
575 | if (this.hlsjsConfig.autoStartLoad === false) { | ||
576 | this.handlers.play = this._startLoad.bind(this) | ||
577 | this.videoElement.addEventListener('play', this.handlers.play) | ||
578 | } | ||
579 | |||
580 | // _notifyVideoQualities sometimes runs before the quality picker event handler is registered -> no video switcher | ||
581 | this.handlers.playing = this._notifyVideoQualities.bind(this) | ||
582 | this.videoElement.addEventListener('playing', this.handlers.playing) | ||
583 | |||
584 | this.hls = new Hlsjs(this.hlsjsConfig) | ||
585 | |||
586 | this._executeHooksFor('beforeinitialize') | ||
587 | |||
588 | this.hls.on(Hlsjs.Events.ERROR, (event, data) => this._onError(event, data)) | ||
589 | this.hls.on(Hlsjs.Events.AUDIO_TRACKS_UPDATED, () => this._onAudioTracks()) | ||
590 | this.hls.on(Hlsjs.Events.MANIFEST_PARSED, (event, data) => this._onMetaData(event, data as any)) // FIXME: typings | ||
591 | this.hls.on(Hlsjs.Events.LEVEL_LOADED, (event, data) => { | ||
592 | // The DVR plugin will auto seek to "live edge" on start up | ||
593 | if (this.hlsjsConfig.liveSyncDuration) { | ||
594 | this.edgeMargin = this.hlsjsConfig.liveSyncDuration | ||
595 | } else if (this.hlsjsConfig.liveSyncDurationCount) { | ||
596 | this.edgeMargin = this.hlsjsConfig.liveSyncDurationCount * data.details.targetduration | ||
597 | } | ||
598 | |||
599 | this.isLive = data.details.live | ||
600 | this.dvrDuration = data.details.totalduration | ||
601 | this._duration = this.isLive ? Infinity : data.details.totalduration | ||
602 | }) | ||
603 | this.hls.once(Hlsjs.Events.FRAG_LOADED, () => { | ||
604 | // Emit custom 'loadedmetadata' event for parity with `videojs-contrib-hls` | ||
605 | // Ref: https://github.com/videojs/videojs-contrib-hls#loadedmetadata | ||
606 | this.tech.trigger('loadedmetadata') | ||
607 | }) | ||
608 | |||
609 | this.hls.attachMedia(this.videoElement) | ||
610 | |||
611 | this.handlers.addtrack = this._updateTextTrackList.bind(this) | ||
612 | this.videoElement.textTracks.addEventListener('addtrack', this.handlers.addtrack) | ||
613 | |||
614 | this.hls.loadSource(this.source.src) | ||
615 | } | ||
616 | |||
617 | private initialize () { | ||
618 | this._initHlsjs() | ||
619 | } | ||
620 | } | ||
621 | |||
622 | export { | ||
623 | Html5Hlsjs, | ||
624 | registerSourceHandler, | ||
625 | registerConfigPlugin | ||
626 | } | ||
diff --git a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts index c3f863f72..e86900faa 100644 --- a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts +++ b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts | |||
@@ -1,16 +1,15 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | // @ts-ignore | 2 | import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../peertube-videojs-typings' |
3 | import * as videojs from 'video.js' | ||
4 | import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo, VideoJSComponentInterface } from '../peertube-videojs-typings' | ||
5 | import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs' | 3 | import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs' |
6 | import { Events, Segment } from 'p2p-media-loader-core' | 4 | import { Events, Segment } from 'p2p-media-loader-core' |
7 | import { timeToInt } from '../utils' | 5 | import { timeToInt } from '../utils' |
6 | import { registerConfigPlugin, registerSourceHandler } from './hls-plugin' | ||
7 | import * as Hlsjs from 'hls.js' | ||
8 | 8 | ||
9 | // videojs-hlsjs-plugin needs videojs in window | 9 | registerConfigPlugin(videojs) |
10 | window['videojs'] = videojs | 10 | registerSourceHandler(videojs) |
11 | require('@streamroot/videojs-hlsjs-plugin') | ||
12 | 11 | ||
13 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | 12 | const Plugin = videojs.getPlugin('plugin') |
14 | class P2pMediaLoaderPlugin extends Plugin { | 13 | class P2pMediaLoaderPlugin extends Plugin { |
15 | 14 | ||
16 | private readonly CONSTANTS = { | 15 | private readonly CONSTANTS = { |
@@ -18,7 +17,7 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
18 | } | 17 | } |
19 | private readonly options: P2PMediaLoaderPluginOptions | 18 | private readonly options: P2PMediaLoaderPluginOptions |
20 | 19 | ||
21 | private hlsjs: any // Don't type hlsjs to not bundle the module | 20 | private hlsjs: Hlsjs |
22 | private p2pEngine: Engine | 21 | private p2pEngine: Engine |
23 | private statsP2PBytes = { | 22 | private statsP2PBytes = { |
24 | pendingDownload: [] as number[], | 23 | pendingDownload: [] as number[], |
@@ -37,12 +36,13 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
37 | 36 | ||
38 | private networkInfoInterval: any | 37 | private networkInfoInterval: any |
39 | 38 | ||
40 | constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) { | 39 | constructor (player: VideoJsPlayer, options?: P2PMediaLoaderPluginOptions) { |
41 | super(player, options) | 40 | super(player) |
42 | 41 | ||
43 | this.options = options | 42 | this.options = options |
44 | 43 | ||
45 | if (!videojs.Html5Hlsjs) { | 44 | // FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080 |
45 | if (!(videojs as any).Html5Hlsjs) { | ||
46 | const message = 'HLS.js does not seem to be supported.' | 46 | const message = 'HLS.js does not seem to be supported.' |
47 | console.warn(message) | 47 | console.warn(message) |
48 | 48 | ||
@@ -50,7 +50,8 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
50 | return | 50 | return |
51 | } | 51 | } |
52 | 52 | ||
53 | videojs.Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => { | 53 | // FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080 |
54 | (videojs as any).Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => { | ||
54 | this.hlsjs = hlsjs | 55 | this.hlsjs = hlsjs |
55 | }) | 56 | }) |
56 | 57 | ||
@@ -84,12 +85,11 @@ class P2pMediaLoaderPlugin extends Plugin { | |||
84 | private initialize () { | 85 | private initialize () { |
85 | initHlsJsPlayer(this.hlsjs) | 86 | initHlsJsPlayer(this.hlsjs) |
86 | 87 | ||
87 | const tech = this.player.tech_ | 88 | // FIXME: typings |
88 | this.p2pEngine = tech.options_.hlsjsConfig.loader.getEngine() | 89 | const options = this.player.tech(true).options_ as any |
90 | this.p2pEngine = options.hlsjsConfig.loader.getEngine() | ||
89 | 91 | ||
90 | // Avoid using constants to not import hls.hs | 92 | this.hlsjs.on(Hlsjs.Events.LEVEL_SWITCHING, (_: any, data: any) => { |
91 | // https://github.com/video-dev/hls.js/blob/master/src/events.js#L37 | ||
92 | this.hlsjs.on('hlsLevelSwitching', (_: any, data: any) => { | ||
93 | this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height }) | 93 | this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height }) |
94 | }) | 94 | }) |
95 | 95 | ||
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index d9e02cd7d..dbf631a5e 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -1,21 +1,26 @@ | |||
1 | import { VideoFile } from '../../../../shared/models/videos' | 1 | import { VideoFile } from '../../../../shared/models/videos' |
2 | // @ts-ignore | 2 | import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js' |
3 | import * as videojs from 'video.js' | ||
4 | import 'videojs-hotkeys' | 3 | import 'videojs-hotkeys' |
5 | import 'videojs-dock' | 4 | import 'videojs-dock' |
6 | import 'videojs-contextmenu-ui' | 5 | import 'videojs-contextmenu-ui' |
7 | import 'videojs-contrib-quality-levels' | 6 | import 'videojs-contrib-quality-levels' |
7 | import './upnext/end-card' | ||
8 | import './upnext/upnext-plugin' | 8 | import './upnext/upnext-plugin' |
9 | import './bezels/bezels-plugin' | 9 | import './bezels/bezels-plugin' |
10 | import './peertube-plugin' | 10 | import './peertube-plugin' |
11 | import './videojs-components/next-video-button' | 11 | import './videojs-components/next-video-button' |
12 | import './videojs-components/p2p-info-button' | ||
12 | import './videojs-components/peertube-link-button' | 13 | import './videojs-components/peertube-link-button' |
14 | import './videojs-components/peertube-load-progress-bar' | ||
13 | import './videojs-components/resolution-menu-button' | 15 | import './videojs-components/resolution-menu-button' |
16 | import './videojs-components/resolution-menu-item' | ||
17 | import './videojs-components/settings-dialog' | ||
14 | import './videojs-components/settings-menu-button' | 18 | import './videojs-components/settings-menu-button' |
15 | import './videojs-components/p2p-info-button' | 19 | import './videojs-components/settings-menu-item' |
16 | import './videojs-components/peertube-load-progress-bar' | 20 | import './videojs-components/settings-panel' |
21 | import './videojs-components/settings-panel-child' | ||
17 | import './videojs-components/theater-button' | 22 | import './videojs-components/theater-button' |
18 | import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions, videojsUntyped } from './peertube-videojs-typings' | 23 | import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions } from './peertube-videojs-typings' |
19 | import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig } from './utils' | 24 | import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig } from './utils' |
20 | import { isDefaultLocale } from '../../../../shared/models/i18n/i18n' | 25 | import { isDefaultLocale } from '../../../../shared/models/i18n/i18n' |
21 | import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' | 26 | import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' |
@@ -24,12 +29,17 @@ import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' | |||
24 | import { getStoredP2PEnabled } from './peertube-player-local-storage' | 29 | import { getStoredP2PEnabled } from './peertube-player-local-storage' |
25 | import { TranslationsManager } from './translations-manager' | 30 | import { TranslationsManager } from './translations-manager' |
26 | 31 | ||
32 | // For VideoJS | ||
33 | (window as any).WebVTT = require('vtt.js/lib/vtt.js').WebVTT; | ||
34 | |||
27 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | 35 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) |
28 | videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' | 36 | (videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed' |
37 | |||
38 | const CaptionsButton = videojs.getComponent('CaptionsButton') as any | ||
29 | // Change Captions to Subtitles/CC | 39 | // Change Captions to Subtitles/CC |
30 | videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC' | 40 | CaptionsButton.prototype.controlText_ = 'Subtitles/CC' |
31 | // We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) | 41 | // We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) |
32 | videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' ' | 42 | CaptionsButton.prototype.label_ = ' ' |
33 | 43 | ||
34 | export type PlayerMode = 'webtorrent' | 'p2p-media-loader' | 44 | export type PlayerMode = 'webtorrent' | 'p2p-media-loader' |
35 | 45 | ||
@@ -92,9 +102,9 @@ export type PeertubePlayerManagerOptions = { | |||
92 | 102 | ||
93 | export class PeertubePlayerManager { | 103 | export class PeertubePlayerManager { |
94 | private static playerElementClassName: string | 104 | private static playerElementClassName: string |
95 | private static onPlayerChange: (player: any) => void | 105 | private static onPlayerChange: (player: VideoJsPlayer) => void |
96 | 106 | ||
97 | static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: any) => void) { | 107 | static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: VideoJsPlayer) => void) { |
98 | let p2pMediaLoader: any | 108 | let p2pMediaLoader: any |
99 | 109 | ||
100 | this.onPlayerChange = onPlayerChange | 110 | this.onPlayerChange = onPlayerChange |
@@ -114,12 +124,12 @@ export class PeertubePlayerManager { | |||
114 | 124 | ||
115 | const self = this | 125 | const self = this |
116 | return new Promise(res => { | 126 | return new Promise(res => { |
117 | videojs(options.common.playerElement, videojsOptions, function (this: any) { | 127 | videojs(options.common.playerElement, videojsOptions, function (this: VideoJsPlayer) { |
118 | const player = this | 128 | const player = this |
119 | 129 | ||
120 | let alreadyFallback = false | 130 | let alreadyFallback = false |
121 | 131 | ||
122 | player.tech_.one('error', () => { | 132 | player.tech(true).one('error', () => { |
123 | if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options) | 133 | if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options) |
124 | alreadyFallback = true | 134 | alreadyFallback = true |
125 | }) | 135 | }) |
@@ -164,7 +174,7 @@ export class PeertubePlayerManager { | |||
164 | const videojsOptions = this.getVideojsOptions(mode, options) | 174 | const videojsOptions = this.getVideojsOptions(mode, options) |
165 | 175 | ||
166 | const self = this | 176 | const self = this |
167 | videojs(newVideoElement, videojsOptions, function (this: any) { | 177 | videojs(newVideoElement, videojsOptions, function (this: VideoJsPlayer) { |
168 | const player = this | 178 | const player = this |
169 | 179 | ||
170 | self.addContextMenu(mode, player, options.common.embedUrl) | 180 | self.addContextMenu(mode, player, options.common.embedUrl) |
@@ -173,7 +183,11 @@ export class PeertubePlayerManager { | |||
173 | }) | 183 | }) |
174 | } | 184 | } |
175 | 185 | ||
176 | private static getVideojsOptions (mode: PlayerMode, options: PeertubePlayerManagerOptions, p2pMediaLoaderModule?: any) { | 186 | private static getVideojsOptions ( |
187 | mode: PlayerMode, | ||
188 | options: PeertubePlayerManagerOptions, | ||
189 | p2pMediaLoaderModule?: any | ||
190 | ): VideoJsPlayerOptions { | ||
177 | const commonOptions = options.common | 191 | const commonOptions = options.common |
178 | 192 | ||
179 | let autoplay = commonOptions.autoplay | 193 | let autoplay = commonOptions.autoplay |
@@ -197,9 +211,9 @@ export class PeertubePlayerManager { | |||
197 | } | 211 | } |
198 | 212 | ||
199 | if (mode === 'p2p-media-loader') { | 213 | if (mode === 'p2p-media-loader') { |
200 | const { streamrootHls } = PeertubePlayerManager.addP2PMediaLoaderOptions(plugins, options, p2pMediaLoaderModule) | 214 | const { hlsjs } = PeertubePlayerManager.addP2PMediaLoaderOptions(plugins, options, p2pMediaLoaderModule) |
201 | 215 | ||
202 | html5 = streamrootHls.html5 | 216 | html5 = hlsjs.html5 |
203 | } | 217 | } |
204 | 218 | ||
205 | if (mode === 'webtorrent') { | 219 | if (mode === 'webtorrent') { |
@@ -213,7 +227,7 @@ export class PeertubePlayerManager { | |||
213 | html5, | 227 | html5, |
214 | 228 | ||
215 | // We don't use text track settings for now | 229 | // We don't use text track settings for now |
216 | textTrackSettings: false, | 230 | textTrackSettings: false as any, // FIXME: typings |
217 | controls: commonOptions.controls !== undefined ? commonOptions.controls : true, | 231 | controls: commonOptions.controls !== undefined ? commonOptions.controls : true, |
218 | loop: commonOptions.loop !== undefined ? commonOptions.loop : false, | 232 | loop: commonOptions.loop !== undefined ? commonOptions.loop : false, |
219 | 233 | ||
@@ -237,7 +251,7 @@ export class PeertubePlayerManager { | |||
237 | peertubeLink: commonOptions.peertubeLink, | 251 | peertubeLink: commonOptions.peertubeLink, |
238 | theaterButton: commonOptions.theaterButton, | 252 | theaterButton: commonOptions.theaterButton, |
239 | nextVideo: commonOptions.nextVideo | 253 | nextVideo: commonOptions.nextVideo |
240 | }) | 254 | }) as any // FIXME: typings |
241 | } | 255 | } |
242 | } | 256 | } |
243 | 257 | ||
@@ -289,7 +303,7 @@ export class PeertubePlayerManager { | |||
289 | swarmId: p2pMediaLoaderOptions.playlistUrl | 303 | swarmId: p2pMediaLoaderOptions.playlistUrl |
290 | } | 304 | } |
291 | } | 305 | } |
292 | const streamrootHls = { | 306 | const hlsjs = { |
293 | levelLabelHandler: (level: { height: number, width: number }) => { | 307 | levelLabelHandler: (level: { height: number, width: number }) => { |
294 | const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === level.height) | 308 | const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === level.height) |
295 | 309 | ||
@@ -308,7 +322,7 @@ export class PeertubePlayerManager { | |||
308 | } | 322 | } |
309 | } | 323 | } |
310 | 324 | ||
311 | const toAssign = { p2pMediaLoader, streamrootHls } | 325 | const toAssign = { p2pMediaLoader, hlsjs } |
312 | Object.assign(plugins, toAssign) | 326 | Object.assign(plugins, toAssign) |
313 | 327 | ||
314 | return toAssign | 328 | return toAssign |
@@ -406,7 +420,7 @@ export class PeertubePlayerManager { | |||
406 | return children | 420 | return children |
407 | } | 421 | } |
408 | 422 | ||
409 | private static addContextMenu (mode: PlayerMode, player: any, videoEmbedUrl: string) { | 423 | private static addContextMenu (mode: PlayerMode, player: VideoJsPlayer, videoEmbedUrl: string) { |
410 | const content = [ | 424 | const content = [ |
411 | { | 425 | { |
412 | label: player.localize('Copy the video URL'), | 426 | label: player.localize('Copy the video URL'), |
@@ -416,9 +430,8 @@ export class PeertubePlayerManager { | |||
416 | }, | 430 | }, |
417 | { | 431 | { |
418 | label: player.localize('Copy the video URL at the current time'), | 432 | label: player.localize('Copy the video URL at the current time'), |
419 | listener: function () { | 433 | listener: function (this: VideoJsPlayer) { |
420 | const player = this as videojs.Player | 434 | copyToClipboard(buildVideoLink({ startTime: this.currentTime() })) |
421 | copyToClipboard(buildVideoLink({ startTime: player.currentTime() })) | ||
422 | } | 435 | } |
423 | }, | 436 | }, |
424 | { | 437 | { |
@@ -432,9 +445,8 @@ export class PeertubePlayerManager { | |||
432 | if (mode === 'webtorrent') { | 445 | if (mode === 'webtorrent') { |
433 | content.push({ | 446 | content.push({ |
434 | label: player.localize('Copy magnet URI'), | 447 | label: player.localize('Copy magnet URI'), |
435 | listener: function () { | 448 | listener: function (this: VideoJsPlayer) { |
436 | const player = this as videojs.Player | 449 | copyToClipboard(this.webtorrent().getCurrentVideoFile().magnetUri) |
437 | copyToClipboard(player.webtorrent().getCurrentVideoFile().magnetUri) | ||
438 | } | 450 | } |
439 | }) | 451 | }) |
440 | } | 452 | } |
@@ -472,7 +484,8 @@ export class PeertubePlayerManager { | |||
472 | return event.key === '>' | 484 | return event.key === '>' |
473 | }, | 485 | }, |
474 | handler: function (player: videojs.Player) { | 486 | handler: function (player: videojs.Player) { |
475 | player.playbackRate((player.playbackRate() + 0.1).toFixed(2)) | 487 | const newValue = Math.min(player.playbackRate() + 0.1, 5) |
488 | player.playbackRate(parseFloat(newValue.toFixed(2))) | ||
476 | } | 489 | } |
477 | }, | 490 | }, |
478 | decreasePlaybackRateKey: { | 491 | decreasePlaybackRateKey: { |
@@ -480,7 +493,8 @@ export class PeertubePlayerManager { | |||
480 | return event.key === '<' | 493 | return event.key === '<' |
481 | }, | 494 | }, |
482 | handler: function (player: videojs.Player) { | 495 | handler: function (player: videojs.Player) { |
483 | player.playbackRate((player.playbackRate() - 0.1).toFixed(2)) | 496 | const newValue = Math.max(player.playbackRate() - 0.1, 0.10) |
497 | player.playbackRate(parseFloat(newValue.toFixed(2))) | ||
484 | } | 498 | } |
485 | }, | 499 | }, |
486 | frameByFrame: { | 500 | frameByFrame: { |
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts index 9824c43b5..19d104676 100644 --- a/client/src/assets/player/peertube-plugin.ts +++ b/client/src/assets/player/peertube-plugin.ts | |||
@@ -1,14 +1,10 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
4 | import './videojs-components/settings-menu-button' | 2 | import './videojs-components/settings-menu-button' |
5 | import { | 3 | import { |
6 | PeerTubePluginOptions, | 4 | PeerTubePluginOptions, |
7 | ResolutionUpdateData, | 5 | ResolutionUpdateData, |
8 | UserWatching, | 6 | UserWatching, |
9 | VideoJSCaption, | 7 | VideoJSCaption |
10 | VideoJSComponentInterface, | ||
11 | videojsUntyped | ||
12 | } from './peertube-videojs-typings' | 8 | } from './peertube-videojs-typings' |
13 | import { isMobile, timeToInt } from './utils' | 9 | import { isMobile, timeToInt } from './utils' |
14 | import { | 10 | import { |
@@ -20,7 +16,8 @@ import { | |||
20 | saveVolumeInStore | 16 | saveVolumeInStore |
21 | } from './peertube-player-local-storage' | 17 | } from './peertube-player-local-storage' |
22 | 18 | ||
23 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | 19 | const Plugin = videojs.getPlugin('plugin') |
20 | |||
24 | class PeerTubePlugin extends Plugin { | 21 | class PeerTubePlugin extends Plugin { |
25 | private readonly videoViewUrl: string | 22 | private readonly videoViewUrl: string |
26 | private readonly videoDuration: number | 23 | private readonly videoDuration: number |
@@ -28,7 +25,6 @@ class PeerTubePlugin extends Plugin { | |||
28 | USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video | 25 | USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video |
29 | } | 26 | } |
30 | 27 | ||
31 | private player: any | ||
32 | private videoCaptions: VideoJSCaption[] | 28 | private videoCaptions: VideoJSCaption[] |
33 | private defaultSubtitle: string | 29 | private defaultSubtitle: string |
34 | 30 | ||
@@ -40,8 +36,8 @@ class PeerTubePlugin extends Plugin { | |||
40 | private mouseInControlBar = false | 36 | private mouseInControlBar = false |
41 | private readonly savedInactivityTimeout: number | 37 | private readonly savedInactivityTimeout: number |
42 | 38 | ||
43 | constructor (player: videojs.Player, options: PeerTubePluginOptions) { | 39 | constructor (player: VideoJsPlayer, options?: PeerTubePluginOptions) { |
44 | super(player, options) | 40 | super(player) |
45 | 41 | ||
46 | this.videoViewUrl = options.videoViewUrl | 42 | this.videoViewUrl = options.videoViewUrl |
47 | this.videoDuration = options.videoDuration | 43 | this.videoDuration = options.videoDuration |
@@ -67,7 +63,7 @@ class PeerTubePlugin extends Plugin { | |||
67 | this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d)) | 63 | this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d)) |
68 | } | 64 | } |
69 | 65 | ||
70 | this.player.tech_.on('loadedqualitydata', () => { | 66 | this.player.tech(true).on('loadedqualitydata', () => { |
71 | setTimeout(() => { | 67 | setTimeout(() => { |
72 | // Replay a resolution change, now we loaded all quality data | 68 | // Replay a resolution change, now we loaded all quality data |
73 | if (this.lastResolutionChange) this.handleResolutionChange(this.lastResolutionChange) | 69 | if (this.lastResolutionChange) this.handleResolutionChange(this.lastResolutionChange) |
@@ -102,7 +98,7 @@ class PeerTubePlugin extends Plugin { | |||
102 | } | 98 | } |
103 | 99 | ||
104 | this.player.textTracks().on('change', () => { | 100 | this.player.textTracks().on('change', () => { |
105 | const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => { | 101 | const showing = this.player.textTracks().tracks_.find(t => { |
106 | return t.kind === 'captions' && t.mode === 'showing' | 102 | return t.kind === 'captions' && t.mode === 'showing' |
107 | }) | 103 | }) |
108 | 104 | ||
@@ -262,7 +258,7 @@ class PeerTubePlugin extends Plugin { | |||
262 | 258 | ||
263 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 | 259 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 |
264 | private initSmoothProgressBar () { | 260 | private initSmoothProgressBar () { |
265 | const SeekBar = videojsUntyped.getComponent('SeekBar') | 261 | const SeekBar = videojs.getComponent('SeekBar') as any |
266 | SeekBar.prototype.getPercent = function getPercent () { | 262 | SeekBar.prototype.getPercent = function getPercent () { |
267 | // Allows for smooth scrubbing, when player can't keep up. | 263 | // Allows for smooth scrubbing, when player can't keep up. |
268 | // const time = (this.player_.scrubbing()) ? | 264 | // const time = (this.player_.scrubbing()) ? |
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts index aad4dbb4f..a4e4c580c 100644 --- a/client/src/assets/player/peertube-videojs-typings.ts +++ b/client/src/assets/player/peertube-videojs-typings.ts | |||
@@ -1,28 +1,81 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
4 | |||
5 | import { PeerTubePlugin } from './peertube-plugin' | 1 | import { PeerTubePlugin } from './peertube-plugin' |
6 | import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' | 2 | import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' |
7 | import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' | 3 | import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' |
8 | import { PlayerMode } from './peertube-player-manager' | 4 | import { PlayerMode } from './peertube-player-manager' |
9 | import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' | 5 | import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' |
10 | import { VideoFile } from '@shared/models' | 6 | import { VideoFile } from '@shared/models' |
7 | import videojs from 'video.js' | ||
8 | import { Config, Level } from 'hls.js' | ||
9 | |||
10 | declare module 'video.js' { | ||
11 | |||
12 | export interface VideoJsPlayer { | ||
13 | srOptions_: HlsjsConfigHandlerOptions | ||
14 | |||
15 | theaterEnabled: boolean | ||
16 | |||
17 | // FIXME: add it to upstream typings | ||
18 | posterImage: { | ||
19 | show (): void | ||
20 | hide (): void | ||
21 | } | ||
22 | |||
23 | handleTechSeeked_ (): void | ||
24 | |||
25 | // Plugins | ||
11 | 26 | ||
12 | declare namespace videojs { | ||
13 | interface Player { | ||
14 | peertube (): PeerTubePlugin | 27 | peertube (): PeerTubePlugin |
28 | |||
15 | webtorrent (): WebTorrentPlugin | 29 | webtorrent (): WebTorrentPlugin |
30 | |||
16 | p2pMediaLoader (): P2pMediaLoaderPlugin | 31 | p2pMediaLoader (): P2pMediaLoaderPlugin |
32 | |||
33 | contextmenuUI (options: any): any | ||
34 | |||
35 | bezels (): void | ||
36 | |||
37 | qualityLevels (): QualityLevels | ||
38 | |||
39 | textTracks (): TextTrackList & { | ||
40 | on: Function | ||
41 | tracks_: { kind: string, mode: string, language: string }[] | ||
42 | } | ||
43 | |||
44 | audioTracks (): AudioTrackList | ||
45 | |||
46 | dock (options: { title: string, description: string }): void | ||
17 | } | 47 | } |
18 | } | 48 | } |
19 | 49 | ||
20 | interface VideoJSComponentInterface { | 50 | export interface VideoJSTechHLS extends videojs.Tech { |
21 | _player: videojs.Player | 51 | hlsProvider: any // FIXME: typings |
52 | } | ||
53 | |||
54 | export interface HlsjsConfigHandlerOptions { | ||
55 | hlsjsConfig?: Config & { cueHandler: any }// FIXME: typings | ||
56 | captionConfig?: any // FIXME: typings | ||
57 | |||
58 | levelLabelHandler?: (level: Level) => string | ||
59 | } | ||
60 | |||
61 | type QualityLevelRepresentation = { | ||
62 | id: number | ||
63 | height: number | ||
64 | |||
65 | label?: string | ||
66 | width?: number | ||
67 | bandwidth?: number | ||
68 | bitrate?: number | ||
22 | 69 | ||
23 | new (player: videojs.Player, options?: any): any | 70 | enabled?: Function |
71 | _enabled: boolean | ||
72 | } | ||
73 | |||
74 | type QualityLevels = QualityLevelRepresentation[] & { | ||
75 | selectedIndex: number | ||
76 | selectedIndex_: number | ||
24 | 77 | ||
25 | registerComponent (name: string, obj: any): any | 78 | addQualityLevel (representation: QualityLevelRepresentation): void |
26 | } | 79 | } |
27 | 80 | ||
28 | type VideoJSCaption = { | 81 | type VideoJSCaption = { |
@@ -78,9 +131,6 @@ type VideoJSPluginOptions = { | |||
78 | p2pMediaLoader?: P2PMediaLoaderPluginOptions | 131 | p2pMediaLoader?: P2PMediaLoaderPluginOptions |
79 | } | 132 | } |
80 | 133 | ||
81 | // videojs typings don't have some method we need | ||
82 | const videojsUntyped = videojs as any | ||
83 | |||
84 | type LoadedQualityData = { | 134 | type LoadedQualityData = { |
85 | qualitySwitchCallback: Function, | 135 | qualitySwitchCallback: Function, |
86 | qualityData: { | 136 | qualityData: { |
@@ -123,13 +173,13 @@ export { | |||
123 | PlayerNetworkInfo, | 173 | PlayerNetworkInfo, |
124 | ResolutionUpdateData, | 174 | ResolutionUpdateData, |
125 | AutoResolutionUpdateData, | 175 | AutoResolutionUpdateData, |
126 | VideoJSComponentInterface, | ||
127 | videojsUntyped, | ||
128 | VideoJSCaption, | 176 | VideoJSCaption, |
129 | UserWatching, | 177 | UserWatching, |
130 | PeerTubePluginOptions, | 178 | PeerTubePluginOptions, |
131 | WebtorrentPluginOptions, | 179 | WebtorrentPluginOptions, |
132 | P2PMediaLoaderPluginOptions, | 180 | P2PMediaLoaderPluginOptions, |
133 | VideoJSPluginOptions, | 181 | VideoJSPluginOptions, |
134 | LoadedQualityData | 182 | LoadedQualityData, |
183 | QualityLevelRepresentation, | ||
184 | QualityLevels | ||
135 | } | 185 | } |
diff --git a/client/src/assets/player/upnext/end-card.ts b/client/src/assets/player/upnext/end-card.ts new file mode 100644 index 000000000..d121a83a9 --- /dev/null +++ b/client/src/assets/player/upnext/end-card.ts | |||
@@ -0,0 +1,155 @@ | |||
1 | import videojs, { VideoJsPlayer } from 'video.js' | ||
2 | |||
3 | function getMainTemplate (options: any) { | ||
4 | return ` | ||
5 | <div class="vjs-upnext-top"> | ||
6 | <span class="vjs-upnext-headtext">${options.headText}</span> | ||
7 | <div class="vjs-upnext-title"></div> | ||
8 | </div> | ||
9 | <div class="vjs-upnext-autoplay-icon"> | ||
10 | <svg height="100%" version="1.1" viewbox="0 0 98 98" width="100%"> | ||
11 | <circle class="vjs-upnext-svg-autoplay-circle" cx="49" cy="49" fill="#000" fill-opacity="0.8" r="48"></circle> | ||
12 | <circle class="vjs-upnext-svg-autoplay-ring" cx="-49" cy="49" fill-opacity="0" r="46.5" stroke="#FFFFFF" stroke-width="4" transform="rotate(-90)"></circle> | ||
13 | <polygon class="vjs-upnext-svg-autoplay-triangle" fill="#fff" points="32,27 72,49 32,71"></polygon></svg> | ||
14 | </div> | ||
15 | <span class="vjs-upnext-bottom"> | ||
16 | <span class="vjs-upnext-cancel"> | ||
17 | <button class="vjs-upnext-cancel-button" tabindex="0" aria-label="Cancel autoplay">${options.cancelText}</button> | ||
18 | </span> | ||
19 | <span class="vjs-upnext-suspended">${options.suspendedText}</span> | ||
20 | </span> | ||
21 | ` | ||
22 | } | ||
23 | |||
24 | export interface EndCardOptions extends videojs.ComponentOptions { | ||
25 | next: Function, | ||
26 | getTitle: () => string | ||
27 | timeout: number | ||
28 | cancelText: string | ||
29 | headText: string | ||
30 | suspendedText: string | ||
31 | condition: () => boolean | ||
32 | suspended: () => boolean | ||
33 | } | ||
34 | |||
35 | const Component = videojs.getComponent('Component') | ||
36 | class EndCard extends Component { | ||
37 | options_: EndCardOptions | ||
38 | |||
39 | dashOffsetTotal = 586 | ||
40 | dashOffsetStart = 293 | ||
41 | interval = 50 | ||
42 | upNextEvents = new videojs.EventTarget() | ||
43 | ticks = 0 | ||
44 | totalTicks: number | ||
45 | |||
46 | container: HTMLDivElement | ||
47 | title: HTMLElement | ||
48 | autoplayRing: HTMLElement | ||
49 | cancelButton: HTMLElement | ||
50 | suspendedMessage: HTMLElement | ||
51 | nextButton: HTMLElement | ||
52 | |||
53 | constructor (player: VideoJsPlayer, options: EndCardOptions) { | ||
54 | super(player, options) | ||
55 | |||
56 | this.totalTicks = this.options_.timeout / this.interval | ||
57 | |||
58 | player.on('ended', (_: any) => { | ||
59 | if (!this.options_.condition()) return | ||
60 | |||
61 | player.addClass('vjs-upnext--showing') | ||
62 | this.showCard((canceled: boolean) => { | ||
63 | player.removeClass('vjs-upnext--showing') | ||
64 | this.container.style.display = 'none' | ||
65 | if (!canceled) { | ||
66 | this.options_.next() | ||
67 | } | ||
68 | }) | ||
69 | }) | ||
70 | |||
71 | player.on('playing', () => { | ||
72 | this.upNextEvents.trigger('playing') | ||
73 | }) | ||
74 | } | ||
75 | |||
76 | createEl () { | ||
77 | const container = super.createEl('div', { | ||
78 | className: 'vjs-upnext-content', | ||
79 | innerHTML: getMainTemplate(this.options_) | ||
80 | }) as HTMLDivElement | ||
81 | |||
82 | this.container = container | ||
83 | container.style.display = 'none' | ||
84 | |||
85 | this.autoplayRing = container.getElementsByClassName('vjs-upnext-svg-autoplay-ring')[0] as HTMLElement | ||
86 | this.title = container.getElementsByClassName('vjs-upnext-title')[0] as HTMLElement | ||
87 | this.cancelButton = container.getElementsByClassName('vjs-upnext-cancel-button')[0] as HTMLElement | ||
88 | this.suspendedMessage = container.getElementsByClassName('vjs-upnext-suspended')[0] as HTMLElement | ||
89 | this.nextButton = container.getElementsByClassName('vjs-upnext-autoplay-icon')[0] as HTMLElement | ||
90 | |||
91 | this.cancelButton.onclick = () => { | ||
92 | this.upNextEvents.trigger('cancel') | ||
93 | } | ||
94 | |||
95 | this.nextButton.onclick = () => { | ||
96 | this.upNextEvents.trigger('next') | ||
97 | } | ||
98 | |||
99 | return container | ||
100 | } | ||
101 | |||
102 | showCard (cb: Function) { | ||
103 | let timeout: any | ||
104 | |||
105 | this.autoplayRing.setAttribute('stroke-dasharray', '' + this.dashOffsetStart) | ||
106 | this.autoplayRing.setAttribute('stroke-dashoffset', '' + -this.dashOffsetStart) | ||
107 | |||
108 | this.title.innerHTML = this.options_.getTitle() | ||
109 | |||
110 | this.upNextEvents.one('cancel', () => { | ||
111 | clearTimeout(timeout) | ||
112 | cb(true) | ||
113 | }) | ||
114 | |||
115 | this.upNextEvents.one('playing', () => { | ||
116 | clearTimeout(timeout) | ||
117 | cb(true) | ||
118 | }) | ||
119 | |||
120 | this.upNextEvents.one('next', () => { | ||
121 | clearTimeout(timeout) | ||
122 | cb(false) | ||
123 | }) | ||
124 | |||
125 | const goToPercent = (percent: number) => { | ||
126 | const newOffset = Math.max(-this.dashOffsetTotal, - this.dashOffsetStart - percent * this.dashOffsetTotal / 2 / 100) | ||
127 | this.autoplayRing.setAttribute('stroke-dashoffset', '' + newOffset) | ||
128 | } | ||
129 | |||
130 | const tick = () => { | ||
131 | goToPercent((this.ticks++) * 100 / this.totalTicks) | ||
132 | } | ||
133 | |||
134 | const update = () => { | ||
135 | if (this.options_.suspended()) { | ||
136 | this.suspendedMessage.innerText = this.options_.suspendedText | ||
137 | goToPercent(0) | ||
138 | this.ticks = 0 | ||
139 | timeout = setTimeout(update.bind(this), 300) // checks once supsended can be a bit longer | ||
140 | } else if (this.ticks >= this.totalTicks) { | ||
141 | clearTimeout(timeout) | ||
142 | cb(false) | ||
143 | } else { | ||
144 | this.suspendedMessage.innerText = '' | ||
145 | tick() | ||
146 | timeout = setTimeout(update.bind(this), this.interval) | ||
147 | } | ||
148 | } | ||
149 | |||
150 | this.container.style.display = 'block' | ||
151 | timeout = setTimeout(update.bind(this), this.interval) | ||
152 | } | ||
153 | } | ||
154 | |||
155 | videojs.registerComponent('EndCard', EndCard) | ||
diff --git a/client/src/assets/player/upnext/upnext-plugin.ts b/client/src/assets/player/upnext/upnext-plugin.ts index a3747b25f..6512fec2c 100644 --- a/client/src/assets/player/upnext/upnext-plugin.ts +++ b/client/src/assets/player/upnext/upnext-plugin.ts | |||
@@ -1,154 +1,11 @@ | |||
1 | // @ts-ignore | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | import * as videojs from 'video.js' | 2 | import { EndCardOptions } from './end-card' |
3 | import { VideoJSComponentInterface } from '../peertube-videojs-typings' | ||
4 | 3 | ||
5 | function getMainTemplate (options: any) { | 4 | const Plugin = videojs.getPlugin('plugin') |
6 | return ` | ||
7 | <div class="vjs-upnext-top"> | ||
8 | <span class="vjs-upnext-headtext">${options.headText}</span> | ||
9 | <div class="vjs-upnext-title"></div> | ||
10 | </div> | ||
11 | <div class="vjs-upnext-autoplay-icon"> | ||
12 | <svg height="100%" version="1.1" viewbox="0 0 98 98" width="100%"> | ||
13 | <circle class="vjs-upnext-svg-autoplay-circle" cx="49" cy="49" fill="#000" fill-opacity="0.8" r="48"></circle> | ||
14 | <circle class="vjs-upnext-svg-autoplay-ring" cx="-49" cy="49" fill-opacity="0" r="46.5" stroke="#FFFFFF" stroke-width="4" transform="rotate(-90)"></circle> | ||
15 | <polygon class="vjs-upnext-svg-autoplay-triangle" fill="#fff" points="32,27 72,49 32,71"></polygon></svg> | ||
16 | </div> | ||
17 | <span class="vjs-upnext-bottom"> | ||
18 | <span class="vjs-upnext-cancel"> | ||
19 | <button class="vjs-upnext-cancel-button" tabindex="0" aria-label="Cancel autoplay">${options.cancelText}</button> | ||
20 | </span> | ||
21 | <span class="vjs-upnext-suspended">${options.suspendedText}</span> | ||
22 | </span> | ||
23 | ` | ||
24 | } | ||
25 | |||
26 | // @ts-ignore-start | ||
27 | const Component = videojs.getComponent('Component') | ||
28 | class EndCard extends Component { | ||
29 | options_: any | ||
30 | dashOffsetTotal = 586 | ||
31 | dashOffsetStart = 293 | ||
32 | interval = 50 | ||
33 | upNextEvents = new videojs.EventTarget() | ||
34 | ticks = 0 | ||
35 | totalTicks: number | ||
36 | |||
37 | container: HTMLElement | ||
38 | title: HTMLElement | ||
39 | autoplayRing: HTMLElement | ||
40 | cancelButton: HTMLElement | ||
41 | suspendedMessage: HTMLElement | ||
42 | nextButton: HTMLElement | ||
43 | |||
44 | constructor (player: videojs.Player, options: any) { | ||
45 | super(player, options) | ||
46 | |||
47 | this.totalTicks = this.options_.timeout / this.interval | ||
48 | |||
49 | player.on('ended', (_: any) => { | ||
50 | if (!this.options_.condition()) return | ||
51 | |||
52 | player.addClass('vjs-upnext--showing') | ||
53 | this.showCard((canceled: boolean) => { | ||
54 | player.removeClass('vjs-upnext--showing') | ||
55 | this.container.style.display = 'none' | ||
56 | if (!canceled) { | ||
57 | this.options_.next() | ||
58 | } | ||
59 | }) | ||
60 | }) | ||
61 | |||
62 | player.on('playing', () => { | ||
63 | this.upNextEvents.trigger('playing') | ||
64 | }) | ||
65 | } | ||
66 | |||
67 | createEl () { | ||
68 | const container = super.createEl('div', { | ||
69 | className: 'vjs-upnext-content', | ||
70 | innerHTML: getMainTemplate(this.options_) | ||
71 | }) | ||
72 | |||
73 | this.container = container | ||
74 | container.style.display = 'none' | ||
75 | |||
76 | this.autoplayRing = container.getElementsByClassName('vjs-upnext-svg-autoplay-ring')[0] | ||
77 | this.title = container.getElementsByClassName('vjs-upnext-title')[0] | ||
78 | this.cancelButton = container.getElementsByClassName('vjs-upnext-cancel-button')[0] | ||
79 | this.suspendedMessage = container.getElementsByClassName('vjs-upnext-suspended')[0] | ||
80 | this.nextButton = container.getElementsByClassName('vjs-upnext-autoplay-icon')[0] | ||
81 | |||
82 | this.cancelButton.onclick = () => { | ||
83 | this.upNextEvents.trigger('cancel') | ||
84 | } | ||
85 | |||
86 | this.nextButton.onclick = () => { | ||
87 | this.upNextEvents.trigger('next') | ||
88 | } | ||
89 | |||
90 | return container | ||
91 | } | ||
92 | 5 | ||
93 | showCard (cb: Function) { | ||
94 | let timeout: any | ||
95 | |||
96 | this.autoplayRing.setAttribute('stroke-dasharray', '' + this.dashOffsetStart) | ||
97 | this.autoplayRing.setAttribute('stroke-dashoffset', '' + -this.dashOffsetStart) | ||
98 | |||
99 | this.title.innerHTML = this.options_.getTitle() | ||
100 | |||
101 | this.upNextEvents.one('cancel', () => { | ||
102 | clearTimeout(timeout) | ||
103 | cb(true) | ||
104 | }) | ||
105 | |||
106 | this.upNextEvents.one('playing', () => { | ||
107 | clearTimeout(timeout) | ||
108 | cb(true) | ||
109 | }) | ||
110 | |||
111 | this.upNextEvents.one('next', () => { | ||
112 | clearTimeout(timeout) | ||
113 | cb(false) | ||
114 | }) | ||
115 | |||
116 | const goToPercent = (percent: number) => { | ||
117 | const newOffset = Math.max(-this.dashOffsetTotal, - this.dashOffsetStart - percent * this.dashOffsetTotal / 2 / 100) | ||
118 | this.autoplayRing.setAttribute('stroke-dashoffset', '' + newOffset) | ||
119 | } | ||
120 | |||
121 | const tick = () => { | ||
122 | goToPercent((this.ticks++) * 100 / this.totalTicks) | ||
123 | } | ||
124 | |||
125 | const update = () => { | ||
126 | if (this.options_.suspended()) { | ||
127 | this.suspendedMessage.innerText = this.options_.suspendedText | ||
128 | goToPercent(0) | ||
129 | this.ticks = 0 | ||
130 | timeout = setTimeout(update.bind(this), 300) // checks once supsended can be a bit longer | ||
131 | } else if (this.ticks >= this.totalTicks) { | ||
132 | clearTimeout(timeout) | ||
133 | cb(false) | ||
134 | } else { | ||
135 | this.suspendedMessage.innerText = '' | ||
136 | tick() | ||
137 | timeout = setTimeout(update.bind(this), this.interval) | ||
138 | } | ||
139 | } | ||
140 | |||
141 | this.container.style.display = 'block' | ||
142 | timeout = setTimeout(update.bind(this), this.interval) | ||
143 | } | ||
144 | } | ||
145 | // @ts-ignore-end | ||
146 | |||
147 | videojs.registerComponent('EndCard', EndCard) | ||
148 | |||
149 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | ||
150 | class UpNextPlugin extends Plugin { | 6 | class UpNextPlugin extends Plugin { |
151 | constructor (player: videojs.Player, options: any = {}) { | 7 | |
8 | constructor (player: VideoJsPlayer, options: Partial<EndCardOptions> = {}) { | ||
152 | const settings = { | 9 | const settings = { |
153 | next: options.next, | 10 | next: options.next, |
154 | getTitle: options.getTitle, | 11 | getTitle: options.getTitle, |
@@ -160,7 +17,7 @@ class UpNextPlugin extends Plugin { | |||
160 | suspended: options.suspended | 17 | suspended: options.suspended |
161 | } | 18 | } |
162 | 19 | ||
163 | super(player, settings) | 20 | super(player) |
164 | 21 | ||
165 | this.player.ready(() => { | 22 | this.player.ready(() => { |
166 | player.addClass('vjs-upnext') | 23 | player.addClass('vjs-upnext') |
diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts index 38f2482eb..fa902e1f1 100644 --- a/client/src/assets/player/utils.ts +++ b/client/src/assets/player/utils.ts | |||
@@ -51,8 +51,9 @@ function buildVideoLink (options: { | |||
51 | : window.location.origin + window.location.pathname.replace('/embed/', '/watch/') | 51 | : window.location.origin + window.location.pathname.replace('/embed/', '/watch/') |
52 | 52 | ||
53 | const params = new URLSearchParams(window.location.search) | 53 | const params = new URLSearchParams(window.location.search) |
54 | // Remove this unused parameter when we are on a playlist page | 54 | // Remove these unused parameters when we are on a playlist page |
55 | params.delete('videoId') | 55 | params.delete('videoId') |
56 | params.delete('resume') | ||
56 | 57 | ||
57 | if (options.startTime) { | 58 | if (options.startTime) { |
58 | const startTimeInt = Math.floor(options.startTime) | 59 | const startTimeInt = Math.floor(options.startTime) |
diff --git a/client/src/assets/player/videojs-components/next-video-button.ts b/client/src/assets/player/videojs-components/next-video-button.ts index bf5c1aba4..bdb245dcc 100644 --- a/client/src/assets/player/videojs-components/next-video-button.ts +++ b/client/src/assets/player/videojs-components/next-video-button.ts | |||
@@ -1,21 +1,25 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | // FIXME: something weird with our path definition in tsconfig and typings | ||
3 | // @ts-ignore | ||
4 | import { Player } from 'video.js' | ||
5 | 2 | ||
6 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 3 | const Button = videojs.getComponent('Button') |
4 | |||
5 | export interface NextVideoButtonOptions extends videojs.ComponentOptions { | ||
6 | handler: Function | ||
7 | } | ||
7 | 8 | ||
8 | class NextVideoButton extends Button { | 9 | class NextVideoButton extends Button { |
10 | private readonly nextVideoButtonOptions: NextVideoButtonOptions | ||
9 | 11 | ||
10 | constructor (player: Player, options: any) { | 12 | constructor (player: VideoJsPlayer, options?: NextVideoButtonOptions) { |
11 | super(player, options) | 13 | super(player, options) |
14 | |||
15 | this.nextVideoButtonOptions = options | ||
12 | } | 16 | } |
13 | 17 | ||
14 | createEl () { | 18 | createEl () { |
15 | const button = videojsUntyped.dom.createEl('button', { | 19 | const button = videojs.dom.createEl('button', { |
16 | className: 'vjs-next-video' | 20 | className: 'vjs-next-video' |
17 | }) | 21 | }) as HTMLButtonElement |
18 | const nextIcon = videojsUntyped.dom.createEl('span', { | 22 | const nextIcon = videojs.dom.createEl('span', { |
19 | className: 'icon icon-next' | 23 | className: 'icon icon-next' |
20 | }) | 24 | }) |
21 | button.appendChild(nextIcon) | 25 | button.appendChild(nextIcon) |
@@ -26,11 +30,8 @@ class NextVideoButton extends Button { | |||
26 | } | 30 | } |
27 | 31 | ||
28 | handleClick () { | 32 | handleClick () { |
29 | this.options_.handler() | 33 | this.nextVideoButtonOptions.handler() |
30 | } | 34 | } |
31 | |||
32 | } | 35 | } |
33 | 36 | ||
34 | NextVideoButton.prototype.controlText_ = 'Next video' | 37 | videojs.registerComponent('NextVideoButton', NextVideoButton) |
35 | |||
36 | NextVideoButton.registerComponent('NextVideoButton', NextVideoButton) | ||
diff --git a/client/src/assets/player/videojs-components/p2p-info-button.ts b/client/src/assets/player/videojs-components/p2p-info-button.ts index 6424787b2..db6806fed 100644 --- a/client/src/assets/player/videojs-components/p2p-info-button.ts +++ b/client/src/assets/player/videojs-components/p2p-info-button.ts | |||
@@ -1,63 +1,64 @@ | |||
1 | import { PlayerNetworkInfo, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | 1 | import { PlayerNetworkInfo } from '../peertube-videojs-typings' |
2 | import videojs from 'video.js' | ||
2 | import { bytes } from '../utils' | 3 | import { bytes } from '../utils' |
3 | 4 | ||
4 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 5 | const Button = videojs.getComponent('Button') |
5 | class P2pInfoButton extends Button { | 6 | class P2pInfoButton extends Button { |
6 | 7 | ||
7 | createEl () { | 8 | createEl () { |
8 | const div = videojsUntyped.dom.createEl('div', { | 9 | const div = videojs.dom.createEl('div', { |
9 | className: 'vjs-peertube' | 10 | className: 'vjs-peertube' |
10 | }) | 11 | }) |
11 | const subDivWebtorrent = videojsUntyped.dom.createEl('div', { | 12 | const subDivWebtorrent = videojs.dom.createEl('div', { |
12 | className: 'vjs-peertube-hidden' // Hide the stats before we get the info | 13 | className: 'vjs-peertube-hidden' // Hide the stats before we get the info |
13 | }) | 14 | }) as HTMLDivElement |
14 | div.appendChild(subDivWebtorrent) | 15 | div.appendChild(subDivWebtorrent) |
15 | 16 | ||
16 | const downloadIcon = videojsUntyped.dom.createEl('span', { | 17 | const downloadIcon = videojs.dom.createEl('span', { |
17 | className: 'icon icon-download' | 18 | className: 'icon icon-download' |
18 | }) | 19 | }) |
19 | subDivWebtorrent.appendChild(downloadIcon) | 20 | subDivWebtorrent.appendChild(downloadIcon) |
20 | 21 | ||
21 | const downloadSpeedText = videojsUntyped.dom.createEl('span', { | 22 | const downloadSpeedText = videojs.dom.createEl('span', { |
22 | className: 'download-speed-text' | 23 | className: 'download-speed-text' |
23 | }) | 24 | }) |
24 | const downloadSpeedNumber = videojsUntyped.dom.createEl('span', { | 25 | const downloadSpeedNumber = videojs.dom.createEl('span', { |
25 | className: 'download-speed-number' | 26 | className: 'download-speed-number' |
26 | }) | 27 | }) |
27 | const downloadSpeedUnit = videojsUntyped.dom.createEl('span') | 28 | const downloadSpeedUnit = videojs.dom.createEl('span') |
28 | downloadSpeedText.appendChild(downloadSpeedNumber) | 29 | downloadSpeedText.appendChild(downloadSpeedNumber) |
29 | downloadSpeedText.appendChild(downloadSpeedUnit) | 30 | downloadSpeedText.appendChild(downloadSpeedUnit) |
30 | subDivWebtorrent.appendChild(downloadSpeedText) | 31 | subDivWebtorrent.appendChild(downloadSpeedText) |
31 | 32 | ||
32 | const uploadIcon = videojsUntyped.dom.createEl('span', { | 33 | const uploadIcon = videojs.dom.createEl('span', { |
33 | className: 'icon icon-upload' | 34 | className: 'icon icon-upload' |
34 | }) | 35 | }) |
35 | subDivWebtorrent.appendChild(uploadIcon) | 36 | subDivWebtorrent.appendChild(uploadIcon) |
36 | 37 | ||
37 | const uploadSpeedText = videojsUntyped.dom.createEl('span', { | 38 | const uploadSpeedText = videojs.dom.createEl('span', { |
38 | className: 'upload-speed-text' | 39 | className: 'upload-speed-text' |
39 | }) | 40 | }) |
40 | const uploadSpeedNumber = videojsUntyped.dom.createEl('span', { | 41 | const uploadSpeedNumber = videojs.dom.createEl('span', { |
41 | className: 'upload-speed-number' | 42 | className: 'upload-speed-number' |
42 | }) | 43 | }) |
43 | const uploadSpeedUnit = videojsUntyped.dom.createEl('span') | 44 | const uploadSpeedUnit = videojs.dom.createEl('span') |
44 | uploadSpeedText.appendChild(uploadSpeedNumber) | 45 | uploadSpeedText.appendChild(uploadSpeedNumber) |
45 | uploadSpeedText.appendChild(uploadSpeedUnit) | 46 | uploadSpeedText.appendChild(uploadSpeedUnit) |
46 | subDivWebtorrent.appendChild(uploadSpeedText) | 47 | subDivWebtorrent.appendChild(uploadSpeedText) |
47 | 48 | ||
48 | const peersText = videojsUntyped.dom.createEl('span', { | 49 | const peersText = videojs.dom.createEl('span', { |
49 | className: 'peers-text' | 50 | className: 'peers-text' |
50 | }) | 51 | }) |
51 | const peersNumber = videojsUntyped.dom.createEl('span', { | 52 | const peersNumber = videojs.dom.createEl('span', { |
52 | className: 'peers-number' | 53 | className: 'peers-number' |
53 | }) | 54 | }) |
54 | subDivWebtorrent.appendChild(peersNumber) | 55 | subDivWebtorrent.appendChild(peersNumber) |
55 | subDivWebtorrent.appendChild(peersText) | 56 | subDivWebtorrent.appendChild(peersText) |
56 | 57 | ||
57 | const subDivHttp = videojsUntyped.dom.createEl('div', { | 58 | const subDivHttp = videojs.dom.createEl('div', { |
58 | className: 'vjs-peertube-hidden' | 59 | className: 'vjs-peertube-hidden' |
59 | }) | 60 | }) |
60 | const subDivHttpText = videojsUntyped.dom.createEl('span', { | 61 | const subDivHttpText = videojs.dom.createEl('span', { |
61 | className: 'http-fallback', | 62 | className: 'http-fallback', |
62 | textContent: 'HTTP' | 63 | textContent: 'HTTP' |
63 | }) | 64 | }) |
@@ -83,8 +84,8 @@ class P2pInfoButton extends Button { | |||
83 | const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded) | 84 | const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded) |
84 | const numPeers = p2pStats.numPeers | 85 | const numPeers = p2pStats.numPeers |
85 | 86 | ||
86 | subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' + | 87 | subDivWebtorrent.title = this.player().localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' + |
87 | this.player_.localize('Total uploaded: ' + totalUploaded.join(' ')) | 88 | this.player().localize('Total uploaded: ' + totalUploaded.join(' ')) |
88 | 89 | ||
89 | downloadSpeedNumber.textContent = downloadSpeed[ 0 ] | 90 | downloadSpeedNumber.textContent = downloadSpeed[ 0 ] |
90 | downloadSpeedUnit.textContent = ' ' + downloadSpeed[ 1 ] | 91 | downloadSpeedUnit.textContent = ' ' + downloadSpeed[ 1 ] |
@@ -92,14 +93,15 @@ class P2pInfoButton extends Button { | |||
92 | uploadSpeedNumber.textContent = uploadSpeed[ 0 ] | 93 | uploadSpeedNumber.textContent = uploadSpeed[ 0 ] |
93 | uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] | 94 | uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] |
94 | 95 | ||
95 | peersNumber.textContent = numPeers | 96 | peersNumber.textContent = numPeers.toString() |
96 | peersText.textContent = ' ' + (numPeers > 1 ? this.player_.localize('peers') : this.player_.localize('peer')) | 97 | peersText.textContent = ' ' + (numPeers > 1 ? this.player().localize('peers') : this.player_.localize('peer')) |
97 | 98 | ||
98 | subDivHttp.className = 'vjs-peertube-hidden' | 99 | subDivHttp.className = 'vjs-peertube-hidden' |
99 | subDivWebtorrent.className = 'vjs-peertube-displayed' | 100 | subDivWebtorrent.className = 'vjs-peertube-displayed' |
100 | }) | 101 | }) |
101 | 102 | ||
102 | return div | 103 | return div as HTMLButtonElement |
103 | } | 104 | } |
104 | } | 105 | } |
105 | Button.registerComponent('P2PInfoButton', P2pInfoButton) | 106 | |
107 | videojs.registerComponent('P2PInfoButton', P2pInfoButton) | ||
diff --git a/client/src/assets/player/videojs-components/peertube-link-button.ts b/client/src/assets/player/videojs-components/peertube-link-button.ts index 4d0ea37f5..0db9762a5 100644 --- a/client/src/assets/player/videojs-components/peertube-link-button.ts +++ b/client/src/assets/player/videojs-components/peertube-link-button.ts | |||
@@ -1,13 +1,10 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | ||
2 | import { buildVideoLink } from '../utils' | 1 | import { buildVideoLink } from '../utils' |
3 | // FIXME: something weird with our path definition in tsconfig and typings | 2 | import videojs, { VideoJsPlayer } from 'video.js' |
4 | // @ts-ignore | ||
5 | import { Player } from 'video.js' | ||
6 | 3 | ||
7 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 4 | const Button = videojs.getComponent('Button') |
8 | class PeerTubeLinkButton extends Button { | 5 | class PeerTubeLinkButton extends Button { |
9 | 6 | ||
10 | constructor (player: Player, options: any) { | 7 | constructor (player: VideoJsPlayer, options?: videojs.ComponentOptions) { |
11 | super(player, options) | 8 | super(player, options) |
12 | } | 9 | } |
13 | 10 | ||
@@ -20,21 +17,22 @@ class PeerTubeLinkButton extends Button { | |||
20 | } | 17 | } |
21 | 18 | ||
22 | handleClick () { | 19 | handleClick () { |
23 | this.player_.pause() | 20 | this.player().pause() |
24 | } | 21 | } |
25 | 22 | ||
26 | private buildElement () { | 23 | private buildElement () { |
27 | const el = videojsUntyped.dom.createEl('a', { | 24 | const el = videojs.dom.createEl('a', { |
28 | href: buildVideoLink(), | 25 | href: buildVideoLink(), |
29 | innerHTML: 'PeerTube', | 26 | innerHTML: 'PeerTube', |
30 | title: this.player_.localize('Go to the video page'), | 27 | title: this.player().localize('Go to the video page'), |
31 | className: 'vjs-peertube-link', | 28 | className: 'vjs-peertube-link', |
32 | target: '_blank' | 29 | target: '_blank' |
33 | }) | 30 | }) |
34 | 31 | ||
35 | el.addEventListener('mouseenter', () => this.updateHref()) | 32 | el.addEventListener('mouseenter', () => this.updateHref()) |
36 | 33 | ||
37 | return el | 34 | return el as HTMLButtonElement |
38 | } | 35 | } |
39 | } | 36 | } |
40 | Button.registerComponent('PeerTubeLinkButton', PeerTubeLinkButton) | 37 | |
38 | videojs.registerComponent('PeerTubeLinkButton', PeerTubeLinkButton) | ||
diff --git a/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts index b594fc1c5..8168e8f2d 100644 --- a/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts +++ b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts | |||
@@ -1,16 +1,12 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | // FIXME: something weird with our path definition in tsconfig and typings | ||
3 | // @ts-ignore | ||
4 | import { Player } from 'video.js' | ||
5 | 2 | ||
6 | const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') | 3 | const Component = videojs.getComponent('Component') |
7 | 4 | ||
8 | class PeerTubeLoadProgressBar extends Component { | 5 | class PeerTubeLoadProgressBar extends Component { |
9 | partEls_: any[] | ||
10 | 6 | ||
11 | constructor (player: Player, options: any) { | 7 | constructor (player: VideoJsPlayer, options?: videojs.ComponentOptions) { |
12 | super(player, options) | 8 | super(player, options) |
13 | this.partEls_ = [] | 9 | |
14 | this.on(player, 'progress', this.update) | 10 | this.on(player, 'progress', this.update) |
15 | } | 11 | } |
16 | 12 | ||
@@ -22,8 +18,6 @@ class PeerTubeLoadProgressBar extends Component { | |||
22 | } | 18 | } |
23 | 19 | ||
24 | dispose () { | 20 | dispose () { |
25 | this.partEls_ = null | ||
26 | |||
27 | super.dispose() | 21 | super.dispose() |
28 | } | 22 | } |
29 | 23 | ||
@@ -31,7 +25,8 @@ class PeerTubeLoadProgressBar extends Component { | |||
31 | const torrent = this.player().webtorrent().getTorrent() | 25 | const torrent = this.player().webtorrent().getTorrent() |
32 | if (!torrent) return | 26 | if (!torrent) return |
33 | 27 | ||
34 | this.el_.style.width = (torrent.progress * 100) + '%' | 28 | // FIXME: typings |
29 | (this.el() as HTMLElement).style.width = (torrent.progress * 100) + '%' | ||
35 | } | 30 | } |
36 | 31 | ||
37 | } | 32 | } |
diff --git a/client/src/assets/player/videojs-components/resolution-menu-button.ts b/client/src/assets/player/videojs-components/resolution-menu-button.ts index 2de3ece19..0fa6272e7 100644 --- a/client/src/assets/player/videojs-components/resolution-menu-button.ts +++ b/client/src/assets/player/videojs-components/resolution-menu-button.ts | |||
@@ -1,22 +1,19 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | // @ts-ignore | ||
3 | import { Player } from 'video.js' | ||
4 | 2 | ||
5 | import { LoadedQualityData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | 3 | import { LoadedQualityData } from '../peertube-videojs-typings' |
6 | import { ResolutionMenuItem } from './resolution-menu-item' | 4 | import { ResolutionMenuItem } from './resolution-menu-item' |
7 | 5 | ||
8 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | 6 | const Menu = videojs.getComponent('Menu') |
9 | const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton') | 7 | const MenuButton = videojs.getComponent('MenuButton') |
10 | class ResolutionMenuButton extends MenuButton { | 8 | class ResolutionMenuButton extends MenuButton { |
11 | label: HTMLElement | 9 | labelEl_: HTMLElement |
12 | labelEl_: any | ||
13 | player: Player | ||
14 | 10 | ||
15 | constructor (player: Player, options: any) { | 11 | constructor (player: VideoJsPlayer, options?: videojs.MenuButtonOptions) { |
16 | super(player, options) | 12 | super(player, options) |
17 | this.player = player | ||
18 | 13 | ||
19 | player.tech_.on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data)) | 14 | this.controlText('Quality') |
15 | |||
16 | player.tech(true).on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data)) | ||
20 | 17 | ||
21 | player.peertube().on('resolutionChange', () => setTimeout(() => this.trigger('updateLabel'), 0)) | 18 | player.peertube().on('resolutionChange', () => setTimeout(() => this.trigger('updateLabel'), 0)) |
22 | } | 19 | } |
@@ -24,9 +21,9 @@ class ResolutionMenuButton extends MenuButton { | |||
24 | createEl () { | 21 | createEl () { |
25 | const el = super.createEl() | 22 | const el = super.createEl() |
26 | 23 | ||
27 | this.labelEl_ = videojsUntyped.dom.createEl('div', { | 24 | this.labelEl_ = videojs.dom.createEl('div', { |
28 | className: 'vjs-resolution-value' | 25 | className: 'vjs-resolution-value' |
29 | }) | 26 | }) as HTMLElement |
30 | 27 | ||
31 | el.appendChild(this.labelEl_) | 28 | el.appendChild(this.labelEl_) |
32 | 29 | ||
@@ -55,7 +52,7 @@ class ResolutionMenuButton extends MenuButton { | |||
55 | 52 | ||
56 | for (const child of children) { | 53 | for (const child of children) { |
57 | if (component !== child) { | 54 | if (component !== child) { |
58 | child.selected(false) | 55 | (child as videojs.MenuItem).selected(false) |
59 | } | 56 | } |
60 | } | 57 | } |
61 | }) | 58 | }) |
@@ -76,7 +73,7 @@ class ResolutionMenuButton extends MenuButton { | |||
76 | if (d.id === -1) continue | 73 | if (d.id === -1) continue |
77 | 74 | ||
78 | const label = d.label === '0p' | 75 | const label = d.label === '0p' |
79 | ? this.player.localize('Audio-only') | 76 | ? this.player().localize('Audio-only') |
80 | : d.label | 77 | : d.label |
81 | 78 | ||
82 | this.menu.addChild(new ResolutionMenuItem( | 79 | this.menu.addChild(new ResolutionMenuItem( |
@@ -110,6 +107,5 @@ class ResolutionMenuButton extends MenuButton { | |||
110 | this.trigger('menuChanged') | 107 | this.trigger('menuChanged') |
111 | } | 108 | } |
112 | } | 109 | } |
113 | ResolutionMenuButton.prototype.controlText_ = 'Quality' | ||
114 | 110 | ||
115 | MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton) | 111 | videojs.registerComponent('ResolutionMenuButton', ResolutionMenuButton) |
diff --git a/client/src/assets/player/videojs-components/resolution-menu-item.ts b/client/src/assets/player/videojs-components/resolution-menu-item.ts index 6c42fefd2..b039c4572 100644 --- a/client/src/assets/player/videojs-components/resolution-menu-item.ts +++ b/client/src/assets/player/videojs-components/resolution-menu-item.ts | |||
@@ -1,12 +1,16 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | // @ts-ignore | 2 | import { AutoResolutionUpdateData, ResolutionUpdateData } from '../peertube-videojs-typings' |
3 | import { Player } from 'video.js' | ||
4 | 3 | ||
5 | import { AutoResolutionUpdateData, ResolutionUpdateData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | 4 | const MenuItem = videojs.getComponent('MenuItem') |
5 | |||
6 | export interface ResolutionMenuItemOptions extends videojs.MenuItemOptions { | ||
7 | labels?: { [id: number]: string } | ||
8 | id: number | ||
9 | callback: Function | ||
10 | } | ||
6 | 11 | ||
7 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | ||
8 | class ResolutionMenuItem extends MenuItem { | 12 | class ResolutionMenuItem extends MenuItem { |
9 | private readonly id: number | 13 | private readonly resolutionId: number |
10 | private readonly label: string | 14 | private readonly label: string |
11 | // Only used for the automatic item | 15 | // Only used for the automatic item |
12 | private readonly labels: { [id: number]: string } | 16 | private readonly labels: { [id: number]: string } |
@@ -15,7 +19,7 @@ class ResolutionMenuItem extends MenuItem { | |||
15 | private autoResolutionPossible: boolean | 19 | private autoResolutionPossible: boolean |
16 | private currentResolutionLabel: string | 20 | private currentResolutionLabel: string |
17 | 21 | ||
18 | constructor (player: Player, options: any) { | 22 | constructor (player: VideoJsPlayer, options?: ResolutionMenuItemOptions) { |
19 | options.selectable = true | 23 | options.selectable = true |
20 | 24 | ||
21 | super(player, options) | 25 | super(player, options) |
@@ -23,40 +27,40 @@ class ResolutionMenuItem extends MenuItem { | |||
23 | this.autoResolutionPossible = true | 27 | this.autoResolutionPossible = true |
24 | this.currentResolutionLabel = '' | 28 | this.currentResolutionLabel = '' |
25 | 29 | ||
30 | this.resolutionId = options.id | ||
26 | this.label = options.label | 31 | this.label = options.label |
27 | this.labels = options.labels | 32 | this.labels = options.labels |
28 | this.id = options.id | ||
29 | this.callback = options.callback | 33 | this.callback = options.callback |
30 | 34 | ||
31 | player.peertube().on('resolutionChange', (_: any, data: ResolutionUpdateData) => this.updateSelection(data)) | 35 | player.peertube().on('resolutionChange', (_: any, data: ResolutionUpdateData) => this.updateSelection(data)) |
32 | 36 | ||
33 | // We only want to disable the "Auto" item | 37 | // We only want to disable the "Auto" item |
34 | if (this.id === -1) { | 38 | if (this.resolutionId === -1) { |
35 | player.peertube().on('autoResolutionChange', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data)) | 39 | player.peertube().on('autoResolutionChange', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data)) |
36 | } | 40 | } |
37 | } | 41 | } |
38 | 42 | ||
39 | handleClick (event: any) { | 43 | handleClick (event: any) { |
40 | // Auto button disabled? | 44 | // Auto button disabled? |
41 | if (this.autoResolutionPossible === false && this.id === -1) return | 45 | if (this.autoResolutionPossible === false && this.resolutionId === -1) return |
42 | 46 | ||
43 | super.handleClick(event) | 47 | super.handleClick(event) |
44 | 48 | ||
45 | this.callback(this.id, 'video') | 49 | this.callback(this.resolutionId, 'video') |
46 | } | 50 | } |
47 | 51 | ||
48 | updateSelection (data: ResolutionUpdateData) { | 52 | updateSelection (data: ResolutionUpdateData) { |
49 | if (this.id === -1) { | 53 | if (this.resolutionId === -1) { |
50 | this.currentResolutionLabel = this.labels[data.id] | 54 | this.currentResolutionLabel = this.labels[data.id] |
51 | } | 55 | } |
52 | 56 | ||
53 | // Automatic resolution only | 57 | // Automatic resolution only |
54 | if (data.auto === true) { | 58 | if (data.auto === true) { |
55 | this.selected(this.id === -1) | 59 | this.selected(this.resolutionId === -1) |
56 | return | 60 | return |
57 | } | 61 | } |
58 | 62 | ||
59 | this.selected(this.id === data.id) | 63 | this.selected(this.resolutionId === data.id) |
60 | } | 64 | } |
61 | 65 | ||
62 | updateAutoResolution (data: AutoResolutionUpdateData) { | 66 | updateAutoResolution (data: AutoResolutionUpdateData) { |
@@ -71,13 +75,13 @@ class ResolutionMenuItem extends MenuItem { | |||
71 | } | 75 | } |
72 | 76 | ||
73 | getLabel () { | 77 | getLabel () { |
74 | if (this.id === -1) { | 78 | if (this.resolutionId === -1) { |
75 | return this.label + ' <small>' + this.currentResolutionLabel + '</small>' | 79 | return this.label + ' <small>' + this.currentResolutionLabel + '</small>' |
76 | } | 80 | } |
77 | 81 | ||
78 | return this.label | 82 | return this.label |
79 | } | 83 | } |
80 | } | 84 | } |
81 | MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) | 85 | videojs.registerComponent('ResolutionMenuItem', ResolutionMenuItem) |
82 | 86 | ||
83 | export { ResolutionMenuItem } | 87 | export { ResolutionMenuItem } |
diff --git a/client/src/assets/player/videojs-components/settings-dialog.ts b/client/src/assets/player/videojs-components/settings-dialog.ts new file mode 100644 index 000000000..dd0b1e472 --- /dev/null +++ b/client/src/assets/player/videojs-components/settings-dialog.ts | |||
@@ -0,0 +1,37 @@ | |||
1 | import videojs, { VideoJsPlayer } from 'video.js' | ||
2 | |||
3 | const Component = videojs.getComponent('Component') | ||
4 | |||
5 | class SettingsDialog extends Component { | ||
6 | constructor (player: VideoJsPlayer) { | ||
7 | super(player) | ||
8 | |||
9 | this.hide() | ||
10 | } | ||
11 | |||
12 | /** | ||
13 | * Create the component's DOM element | ||
14 | * | ||
15 | * @return {Element} | ||
16 | * @method createEl | ||
17 | */ | ||
18 | createEl () { | ||
19 | const uniqueId = this.id() | ||
20 | const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId | ||
21 | const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId | ||
22 | |||
23 | return super.createEl('div', { | ||
24 | className: 'vjs-settings-dialog vjs-modal-overlay', | ||
25 | innerHTML: '', | ||
26 | tabIndex: -1 | ||
27 | }, { | ||
28 | 'role': 'dialog', | ||
29 | 'aria-labelledby': dialogLabelId, | ||
30 | 'aria-describedby': dialogDescriptionId | ||
31 | }) | ||
32 | } | ||
33 | } | ||
34 | |||
35 | Component.registerComponent('SettingsDialog', SettingsDialog) | ||
36 | |||
37 | export { SettingsDialog } | ||
diff --git a/client/src/assets/player/videojs-components/settings-menu-button.ts b/client/src/assets/player/videojs-components/settings-menu-button.ts index b700f4be6..eae628e7d 100644 --- a/client/src/assets/player/videojs-components/settings-menu-button.ts +++ b/client/src/assets/player/videojs-components/settings-menu-button.ts | |||
@@ -1,43 +1,52 @@ | |||
1 | // Author: Yanko Shterev | 1 | // Thanks to Yanko Shterev: https://github.com/yshterev/videojs-settings-menu |
2 | // Thanks https://github.com/yshterev/videojs-settings-menu | ||
3 | |||
4 | // FIXME: something weird with our path definition in tsconfig and typings | ||
5 | // @ts-ignore | ||
6 | import * as videojs from 'video.js' | ||
7 | |||
8 | import { SettingsMenuItem } from './settings-menu-item' | 2 | import { SettingsMenuItem } from './settings-menu-item' |
9 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | ||
10 | import { toTitleCase } from '../utils' | 3 | import { toTitleCase } from '../utils' |
4 | import videojs, { VideoJsPlayer } from 'video.js' | ||
5 | |||
6 | import { SettingsDialog } from './settings-dialog' | ||
7 | import { SettingsPanel } from './settings-panel' | ||
8 | import { SettingsPanelChild } from './settings-panel-child' | ||
11 | 9 | ||
12 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 10 | const Button = videojs.getComponent('Button') |
13 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | 11 | const Menu = videojs.getComponent('Menu') |
14 | const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') | 12 | const Component = videojs.getComponent('Component') |
13 | |||
14 | export interface SettingsButtonOptions extends videojs.ComponentOptions { | ||
15 | entries: any[] | ||
16 | setup?: { | ||
17 | maxHeightOffset: number | ||
18 | } | ||
19 | } | ||
15 | 20 | ||
16 | class SettingsButton extends Button { | 21 | class SettingsButton extends Button { |
17 | playerComponent = videojs.Player | 22 | dialog: SettingsDialog |
18 | dialog: any | 23 | dialogEl: HTMLElement |
19 | dialogEl: any | 24 | menu: videojs.Menu |
20 | menu: any | 25 | panel: SettingsPanel |
21 | panel: any | 26 | panelChild: SettingsPanelChild |
22 | panelChild: any | 27 | |
23 | 28 | addSettingsItemHandler: typeof SettingsButton.prototype.onAddSettingsItem | |
24 | addSettingsItemHandler: Function | 29 | disposeSettingsItemHandler: typeof SettingsButton.prototype.onDisposeSettingsItem |
25 | disposeSettingsItemHandler: Function | 30 | playerClickHandler: typeof SettingsButton.prototype.onPlayerClick |
26 | playerClickHandler: Function | 31 | userInactiveHandler: typeof SettingsButton.prototype.onUserInactive |
27 | userInactiveHandler: Function | 32 | |
28 | 33 | private settingsButtonOptions: SettingsButtonOptions | |
29 | constructor (player: videojs.Player, options: any) { | 34 | |
35 | constructor (player: VideoJsPlayer, options?: SettingsButtonOptions) { | ||
30 | super(player, options) | 36 | super(player, options) |
31 | 37 | ||
32 | this.playerComponent = player | 38 | this.settingsButtonOptions = options |
33 | this.dialog = this.playerComponent.addChild('settingsDialog') | 39 | |
34 | this.dialogEl = this.dialog.el_ | 40 | this.controlText('Settings') |
41 | |||
42 | this.dialog = this.player().addChild('settingsDialog') | ||
43 | this.dialogEl = this.dialog.el() as HTMLElement | ||
35 | this.menu = null | 44 | this.menu = null |
36 | this.panel = this.dialog.addChild('settingsPanel') | 45 | this.panel = this.dialog.addChild('settingsPanel') |
37 | this.panelChild = this.panel.addChild('settingsPanelChild') | 46 | this.panelChild = this.panel.addChild('settingsPanelChild') |
38 | 47 | ||
39 | this.addClass('vjs-settings') | 48 | this.addClass('vjs-settings') |
40 | this.el_.setAttribute('aria-label', 'Settings Button') | 49 | this.el().setAttribute('aria-label', 'Settings Button') |
41 | 50 | ||
42 | // Event handlers | 51 | // Event handlers |
43 | this.addSettingsItemHandler = this.onAddSettingsItem.bind(this) | 52 | this.addSettingsItemHandler = this.onAddSettingsItem.bind(this) |
@@ -84,7 +93,7 @@ class SettingsButton extends Button { | |||
84 | 93 | ||
85 | this.hideDialog() | 94 | this.hideDialog() |
86 | 95 | ||
87 | if (this.options_.entries.length === 0) { | 96 | if (this.settingsButtonOptions.entries.length === 0) { |
88 | this.addClass('vjs-hidden') | 97 | this.addClass('vjs-hidden') |
89 | } | 98 | } |
90 | } | 99 | } |
@@ -103,10 +112,10 @@ class SettingsButton extends Button { | |||
103 | } | 112 | } |
104 | 113 | ||
105 | bindEvents () { | 114 | bindEvents () { |
106 | this.playerComponent.on('click', this.playerClickHandler) | 115 | this.player().on('click', this.playerClickHandler) |
107 | this.playerComponent.on('addsettingsitem', this.addSettingsItemHandler) | 116 | this.player().on('addsettingsitem', this.addSettingsItemHandler) |
108 | this.playerComponent.on('disposesettingsitem', this.disposeSettingsItemHandler) | 117 | this.player().on('disposesettingsitem', this.disposeSettingsItemHandler) |
109 | this.playerComponent.on('userinactive', this.userInactiveHandler) | 118 | this.player().on('userinactive', this.userInactiveHandler) |
110 | } | 119 | } |
111 | 120 | ||
112 | buildCSSClass () { | 121 | buildCSSClass () { |
@@ -122,9 +131,9 @@ class SettingsButton extends Button { | |||
122 | } | 131 | } |
123 | 132 | ||
124 | showDialog () { | 133 | showDialog () { |
125 | this.player_.peertube().onMenuOpen() | 134 | this.player().peertube().onMenuOpen(); |
126 | 135 | ||
127 | this.menu.el_.style.opacity = '1' | 136 | (this.menu.el() as HTMLElement).style.opacity = '1' |
128 | this.dialog.show() | 137 | this.dialog.show() |
129 | 138 | ||
130 | this.setDialogSize(this.getComponentSize(this.menu)) | 139 | this.setDialogSize(this.getComponentSize(this.menu)) |
@@ -134,23 +143,24 @@ class SettingsButton extends Button { | |||
134 | this.player_.peertube().onMenuClosed() | 143 | this.player_.peertube().onMenuClosed() |
135 | 144 | ||
136 | this.dialog.hide() | 145 | this.dialog.hide() |
137 | this.setDialogSize(this.getComponentSize(this.menu)) | 146 | this.setDialogSize(this.getComponentSize(this.menu)); |
138 | this.menu.el_.style.opacity = '1' | 147 | (this.menu.el() as HTMLElement).style.opacity = '1' |
139 | this.resetChildren() | 148 | this.resetChildren() |
140 | } | 149 | } |
141 | 150 | ||
142 | getComponentSize (element: any) { | 151 | getComponentSize (element: videojs.Component | HTMLElement) { |
143 | let width: number = null | 152 | let width: number = null |
144 | let height: number = null | 153 | let height: number = null |
145 | 154 | ||
146 | // Could be component or just DOM element | 155 | // Could be component or just DOM element |
147 | if (element instanceof Component) { | 156 | if (element instanceof Component) { |
148 | width = element.el_.offsetWidth | 157 | const el = element.el() as HTMLElement |
149 | height = element.el_.offsetHeight | 158 | |
159 | width = el.offsetWidth | ||
160 | height = el.offsetHeight; | ||
150 | 161 | ||
151 | // keep width/height as properties for direct use | 162 | (element as any).width = width; |
152 | element.width = width | 163 | (element as any).height = height |
153 | element.height = height | ||
154 | } else { | 164 | } else { |
155 | width = element.offsetWidth | 165 | width = element.offsetWidth |
156 | height = element.offsetHeight | 166 | height = element.offsetHeight |
@@ -164,15 +174,17 @@ class SettingsButton extends Button { | |||
164 | return | 174 | return |
165 | } | 175 | } |
166 | 176 | ||
167 | const offset = this.options_.setup.maxHeightOffset | 177 | const offset = this.settingsButtonOptions.setup.maxHeightOffset |
168 | const maxHeight = this.playerComponent.el_.offsetHeight - offset | 178 | const maxHeight = (this.player().el() as HTMLElement).offsetHeight - offset // FIXME: typings |
179 | |||
180 | const panelEl = this.panel.el() as HTMLElement | ||
169 | 181 | ||
170 | if (height > maxHeight) { | 182 | if (height > maxHeight) { |
171 | height = maxHeight | 183 | height = maxHeight |
172 | width += 17 | 184 | width += 17 |
173 | this.panel.el_.style.maxHeight = `${height}px` | 185 | panelEl.style.maxHeight = `${height}px` |
174 | } else if (this.panel.el_.style.maxHeight !== '') { | 186 | } else if (panelEl.style.maxHeight !== '') { |
175 | this.panel.el_.style.maxHeight = '' | 187 | panelEl.style.maxHeight = '' |
176 | } | 188 | } |
177 | 189 | ||
178 | this.dialogEl.style.width = `${width}px` | 190 | this.dialogEl.style.width = `${width}px` |
@@ -182,7 +194,7 @@ class SettingsButton extends Button { | |||
182 | buildMenu () { | 194 | buildMenu () { |
183 | this.menu = new Menu(this.player()) | 195 | this.menu = new Menu(this.player()) |
184 | this.menu.addClass('vjs-main-menu') | 196 | this.menu.addClass('vjs-main-menu') |
185 | const entries = this.options_.entries | 197 | const entries = this.settingsButtonOptions.entries |
186 | 198 | ||
187 | if (entries.length === 0) { | 199 | if (entries.length === 0) { |
188 | this.addClass('vjs-hidden') | 200 | this.addClass('vjs-hidden') |
@@ -191,7 +203,7 @@ class SettingsButton extends Button { | |||
191 | } | 203 | } |
192 | 204 | ||
193 | for (const entry of entries) { | 205 | for (const entry of entries) { |
194 | this.addMenuItem(entry, this.options_) | 206 | this.addMenuItem(entry, this.settingsButtonOptions) |
195 | } | 207 | } |
196 | 208 | ||
197 | this.panelChild.addChild(this.menu) | 209 | this.panelChild.addChild(this.menu) |
@@ -199,15 +211,17 @@ class SettingsButton extends Button { | |||
199 | 211 | ||
200 | addMenuItem (entry: any, options: any) { | 212 | addMenuItem (entry: any, options: any) { |
201 | const openSubMenu = function (this: any) { | 213 | const openSubMenu = function (this: any) { |
202 | if (videojsUntyped.dom.hasClass(this.el_, 'open')) { | 214 | if (videojs.dom.hasClass(this.el_, 'open')) { |
203 | videojsUntyped.dom.removeClass(this.el_, 'open') | 215 | videojs.dom.removeClass(this.el_, 'open') |
204 | } else { | 216 | } else { |
205 | videojsUntyped.dom.addClass(this.el_, 'open') | 217 | videojs.dom.addClass(this.el_, 'open') |
206 | } | 218 | } |
207 | } | 219 | } |
208 | 220 | ||
209 | options.name = toTitleCase(entry) | 221 | options.name = toTitleCase(entry) |
210 | const settingsMenuItem = new SettingsMenuItem(this.player(), options, entry, this as any) | 222 | |
223 | const newOptions = Object.assign({}, options, { entry, menuButton: this }) | ||
224 | const settingsMenuItem = new SettingsMenuItem(this.player(), newOptions) | ||
211 | 225 | ||
212 | this.menu.addChild(settingsMenuItem) | 226 | this.menu.addChild(settingsMenuItem) |
213 | 227 | ||
@@ -221,7 +235,7 @@ class SettingsButton extends Button { | |||
221 | 235 | ||
222 | resetChildren () { | 236 | resetChildren () { |
223 | for (const menuChild of this.menu.children()) { | 237 | for (const menuChild of this.menu.children()) { |
224 | menuChild.reset() | 238 | (menuChild as SettingsMenuItem).reset() |
225 | } | 239 | } |
226 | } | 240 | } |
227 | 241 | ||
@@ -230,75 +244,12 @@ class SettingsButton extends Button { | |||
230 | */ | 244 | */ |
231 | hideChildren () { | 245 | hideChildren () { |
232 | for (const menuChild of this.menu.children()) { | 246 | for (const menuChild of this.menu.children()) { |
233 | menuChild.hideSubMenu() | 247 | (menuChild as SettingsMenuItem).hideSubMenu() |
234 | } | 248 | } |
235 | } | 249 | } |
236 | 250 | ||
237 | } | 251 | } |
238 | 252 | ||
239 | class SettingsPanel extends Component { | ||
240 | constructor (player: videojs.Player, options: any) { | ||
241 | super(player, options) | ||
242 | } | ||
243 | |||
244 | createEl () { | ||
245 | return super.createEl('div', { | ||
246 | className: 'vjs-settings-panel', | ||
247 | innerHTML: '', | ||
248 | tabIndex: -1 | ||
249 | }) | ||
250 | } | ||
251 | } | ||
252 | |||
253 | class SettingsPanelChild extends Component { | ||
254 | constructor (player: videojs.Player, options: any) { | ||
255 | super(player, options) | ||
256 | } | ||
257 | |||
258 | createEl () { | ||
259 | return super.createEl('div', { | ||
260 | className: 'vjs-settings-panel-child', | ||
261 | innerHTML: '', | ||
262 | tabIndex: -1 | ||
263 | }) | ||
264 | } | ||
265 | } | ||
266 | |||
267 | class SettingsDialog extends Component { | ||
268 | constructor (player: videojs.Player, options: any) { | ||
269 | super(player, options) | ||
270 | this.hide() | ||
271 | } | ||
272 | |||
273 | /** | ||
274 | * Create the component's DOM element | ||
275 | * | ||
276 | * @return {Element} | ||
277 | * @method createEl | ||
278 | */ | ||
279 | createEl () { | ||
280 | const uniqueId = this.id_ | ||
281 | const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId | ||
282 | const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId | ||
283 | |||
284 | return super.createEl('div', { | ||
285 | className: 'vjs-settings-dialog vjs-modal-overlay', | ||
286 | innerHTML: '', | ||
287 | tabIndex: -1 | ||
288 | }, { | ||
289 | 'role': 'dialog', | ||
290 | 'aria-labelledby': dialogLabelId, | ||
291 | 'aria-describedby': dialogDescriptionId | ||
292 | }) | ||
293 | } | ||
294 | |||
295 | } | ||
296 | |||
297 | SettingsButton.prototype.controlText_ = 'Settings' | ||
298 | |||
299 | Component.registerComponent('SettingsButton', SettingsButton) | 253 | Component.registerComponent('SettingsButton', SettingsButton) |
300 | Component.registerComponent('SettingsDialog', SettingsDialog) | ||
301 | Component.registerComponent('SettingsPanel', SettingsPanel) | ||
302 | Component.registerComponent('SettingsPanelChild', SettingsPanelChild) | ||
303 | 254 | ||
304 | export { SettingsButton, SettingsDialog, SettingsPanel, SettingsPanelChild } | 255 | export { SettingsButton } |
diff --git a/client/src/assets/player/videojs-components/settings-menu-item.ts b/client/src/assets/player/videojs-components/settings-menu-item.ts index 84d394c0e..f5671f49d 100644 --- a/client/src/assets/player/videojs-components/settings-menu-item.ts +++ b/client/src/assets/player/videojs-components/settings-menu-item.ts | |||
@@ -1,57 +1,63 @@ | |||
1 | // Author: Yanko Shterev | 1 | // Thanks to Yanko Shterev: https://github.com/yshterev/videojs-settings-menu |
2 | // Thanks https://github.com/yshterev/videojs-settings-menu | ||
3 | |||
4 | // FIXME: something weird with our path definition in tsconfig and typings | ||
5 | // @ts-ignore | ||
6 | import * as videojs from 'video.js' | ||
7 | |||
8 | import { toTitleCase } from '../utils' | 2 | import { toTitleCase } from '../utils' |
9 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | 3 | import videojs, { VideoJsPlayer } from 'video.js' |
10 | 4 | import { SettingsButton } from './settings-menu-button' | |
11 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | 5 | import { SettingsDialog } from './settings-dialog' |
12 | const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') | 6 | import { SettingsPanel } from './settings-panel' |
7 | import { SettingsPanelChild } from './settings-panel-child' | ||
8 | |||
9 | const MenuItem = videojs.getComponent('MenuItem') | ||
10 | const component = videojs.getComponent('Component') | ||
11 | |||
12 | export interface SettingsMenuItemOptions extends videojs.MenuItemOptions { | ||
13 | entry: string | ||
14 | menuButton: SettingsButton | ||
15 | } | ||
13 | 16 | ||
14 | class SettingsMenuItem extends MenuItem { | 17 | class SettingsMenuItem extends MenuItem { |
15 | settingsButton: any | 18 | settingsButton: SettingsButton |
16 | dialog: any | 19 | dialog: SettingsDialog |
17 | mainMenu: any | 20 | mainMenu: videojs.Menu |
18 | panel: any | 21 | panel: SettingsPanel |
19 | panelChild: any | 22 | panelChild: SettingsPanelChild |
20 | panelChildEl: any | 23 | panelChildEl: HTMLElement |
21 | size: any | 24 | size: number[] |
22 | menuToLoad: string | 25 | menuToLoad: string |
23 | subMenu: any | 26 | subMenu: SettingsButton |
24 | 27 | ||
25 | submenuClickHandler: Function | 28 | submenuClickHandler: typeof SettingsMenuItem.prototype.onSubmenuClick |
26 | transitionEndHandler: Function | 29 | transitionEndHandler: typeof SettingsMenuItem.prototype.onTransitionEnd |
27 | 30 | ||
28 | settingsSubMenuTitleEl_: any | 31 | settingsSubMenuTitleEl_: HTMLElement |
29 | settingsSubMenuValueEl_: any | 32 | settingsSubMenuValueEl_: HTMLElement |
30 | settingsSubMenuEl_: any | 33 | settingsSubMenuEl_: HTMLElement |
31 | 34 | ||
32 | constructor (player: videojs.Player, options: any, entry: string, menuButton: VideoJSComponentInterface) { | 35 | constructor (player: VideoJsPlayer, options?: SettingsMenuItemOptions) { |
33 | super(player, options) | 36 | super(player, options) |
34 | 37 | ||
35 | this.settingsButton = menuButton | 38 | this.settingsButton = options.menuButton |
36 | this.dialog = this.settingsButton.dialog | 39 | this.dialog = this.settingsButton.dialog |
37 | this.mainMenu = this.settingsButton.menu | 40 | this.mainMenu = this.settingsButton.menu |
38 | this.panel = this.dialog.getChild('settingsPanel') | 41 | this.panel = this.dialog.getChild('settingsPanel') |
39 | this.panelChild = this.panel.getChild('settingsPanelChild') | 42 | this.panelChild = this.panel.getChild('settingsPanelChild') |
40 | this.panelChildEl = this.panelChild.el_ | 43 | this.panelChildEl = this.panelChild.el() as HTMLElement |
41 | 44 | ||
42 | this.size = null | 45 | this.size = null |
43 | 46 | ||
44 | // keep state of what menu type is loading next | 47 | // keep state of what menu type is loading next |
45 | this.menuToLoad = 'mainmenu' | 48 | this.menuToLoad = 'mainmenu' |
46 | 49 | ||
47 | const subMenuName = toTitleCase(entry) | 50 | const subMenuName = toTitleCase(options.entry) |
48 | const SubMenuComponent = videojsUntyped.getComponent(subMenuName) | 51 | const SubMenuComponent = videojs.getComponent(subMenuName) |
49 | 52 | ||
50 | if (!SubMenuComponent) { | 53 | if (!SubMenuComponent) { |
51 | throw new Error(`Component ${subMenuName} does not exist`) | 54 | throw new Error(`Component ${subMenuName} does not exist`) |
52 | } | 55 | } |
53 | this.subMenu = new SubMenuComponent(this.player(), options, menuButton, this) | 56 | |
54 | const subMenuClass = this.subMenu.buildCSSClass().split(' ')[0] | 57 | const newOptions = Object.assign({}, options, { entry: options.menuButton, menuButton: this }) |
58 | |||
59 | this.subMenu = new SubMenuComponent(this.player(), newOptions) as any // FIXME: typings | ||
60 | const subMenuClass = this.subMenu.buildCSSClass().split(' ')[ 0 ] | ||
55 | this.settingsSubMenuEl_.className += ' ' + subMenuClass | 61 | this.settingsSubMenuEl_.className += ' ' + subMenuClass |
56 | 62 | ||
57 | this.eventHandlers() | 63 | this.eventHandlers() |
@@ -72,7 +78,7 @@ class SettingsMenuItem extends MenuItem { | |||
72 | player.on('captionsChanged', () => { | 78 | player.on('captionsChanged', () => { |
73 | setTimeout(() => { | 79 | setTimeout(() => { |
74 | this.settingsSubMenuEl_.innerHTML = '' | 80 | this.settingsSubMenuEl_.innerHTML = '' |
75 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) | 81 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el()) |
76 | this.update() | 82 | this.update() |
77 | this.bindClickEvents() | 83 | this.bindClickEvents() |
78 | }, 0) | 84 | }, 0) |
@@ -119,27 +125,27 @@ class SettingsMenuItem extends MenuItem { | |||
119 | * @method createEl | 125 | * @method createEl |
120 | */ | 126 | */ |
121 | createEl () { | 127 | createEl () { |
122 | const el = videojsUntyped.dom.createEl('li', { | 128 | const el = videojs.dom.createEl('li', { |
123 | className: 'vjs-menu-item' | 129 | className: 'vjs-menu-item' |
124 | }) | 130 | }) |
125 | 131 | ||
126 | this.settingsSubMenuTitleEl_ = videojsUntyped.dom.createEl('div', { | 132 | this.settingsSubMenuTitleEl_ = videojs.dom.createEl('div', { |
127 | className: 'vjs-settings-sub-menu-title' | 133 | className: 'vjs-settings-sub-menu-title' |
128 | }) | 134 | }) as HTMLElement |
129 | 135 | ||
130 | el.appendChild(this.settingsSubMenuTitleEl_) | 136 | el.appendChild(this.settingsSubMenuTitleEl_) |
131 | 137 | ||
132 | this.settingsSubMenuValueEl_ = videojsUntyped.dom.createEl('div', { | 138 | this.settingsSubMenuValueEl_ = videojs.dom.createEl('div', { |
133 | className: 'vjs-settings-sub-menu-value' | 139 | className: 'vjs-settings-sub-menu-value' |
134 | }) | 140 | }) as HTMLElement |
135 | 141 | ||
136 | el.appendChild(this.settingsSubMenuValueEl_) | 142 | el.appendChild(this.settingsSubMenuValueEl_) |
137 | 143 | ||
138 | this.settingsSubMenuEl_ = videojsUntyped.dom.createEl('div', { | 144 | this.settingsSubMenuEl_ = videojs.dom.createEl('div', { |
139 | className: 'vjs-settings-sub-menu' | 145 | className: 'vjs-settings-sub-menu' |
140 | }) | 146 | }) as HTMLElement |
141 | 147 | ||
142 | return el | 148 | return el as HTMLLIElement |
143 | } | 149 | } |
144 | 150 | ||
145 | /** | 151 | /** |
@@ -147,17 +153,17 @@ class SettingsMenuItem extends MenuItem { | |||
147 | * | 153 | * |
148 | * @method handleClick | 154 | * @method handleClick |
149 | */ | 155 | */ |
150 | handleClick () { | 156 | handleClick (event: videojs.EventTarget.Event) { |
151 | this.menuToLoad = 'submenu' | 157 | this.menuToLoad = 'submenu' |
152 | // Remove open class to ensure only the open submenu gets this class | 158 | // Remove open class to ensure only the open submenu gets this class |
153 | videojsUntyped.dom.removeClass(this.el_, 'open') | 159 | videojs.dom.removeClass(this.el(), 'open') |
154 | 160 | ||
155 | super.handleClick() | 161 | super.handleClick(event); |
156 | 162 | ||
157 | this.mainMenu.el_.style.opacity = '0' | 163 | (this.mainMenu.el() as HTMLElement).style.opacity = '0' |
158 | // Whether to add or remove vjs-hidden class on the settingsSubMenuEl element | 164 | // Whether to add or remove vjs-hidden class on the settingsSubMenuEl element |
159 | if (videojsUntyped.dom.hasClass(this.settingsSubMenuEl_, 'vjs-hidden')) { | 165 | if (videojs.dom.hasClass(this.settingsSubMenuEl_, 'vjs-hidden')) { |
160 | videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') | 166 | videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') |
161 | 167 | ||
162 | // animation not played without timeout | 168 | // animation not played without timeout |
163 | setTimeout(() => { | 169 | setTimeout(() => { |
@@ -167,7 +173,7 @@ class SettingsMenuItem extends MenuItem { | |||
167 | 173 | ||
168 | this.settingsButton.setDialogSize(this.size) | 174 | this.settingsButton.setDialogSize(this.size) |
169 | } else { | 175 | } else { |
170 | videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | 176 | videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') |
171 | } | 177 | } |
172 | } | 178 | } |
173 | 179 | ||
@@ -178,9 +184,9 @@ class SettingsMenuItem extends MenuItem { | |||
178 | */ | 184 | */ |
179 | createBackButton () { | 185 | createBackButton () { |
180 | const button = this.subMenu.menu.addChild('MenuItem', {}, 0) | 186 | const button = this.subMenu.menu.addChild('MenuItem', {}, 0) |
181 | button.name_ = 'BackButton' | 187 | |
182 | button.addClass('vjs-back-button') | 188 | button.addClass('vjs-back-button'); |
183 | button.el_.innerHTML = this.player_.localize(this.subMenu.controlText_) | 189 | (button.el() as HTMLElement).innerHTML = this.player().localize(this.subMenu.controlText()) |
184 | } | 190 | } |
185 | 191 | ||
186 | /** | 192 | /** |
@@ -189,17 +195,17 @@ class SettingsMenuItem extends MenuItem { | |||
189 | * @method PrefixedEvent | 195 | * @method PrefixedEvent |
190 | */ | 196 | */ |
191 | PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') { | 197 | PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') { |
192 | const prefix = ['webkit', 'moz', 'MS', 'o', ''] | 198 | const prefix = [ 'webkit', 'moz', 'MS', 'o', '' ] |
193 | 199 | ||
194 | for (let p = 0; p < prefix.length; p++) { | 200 | for (let p = 0; p < prefix.length; p++) { |
195 | if (!prefix[p]) { | 201 | if (!prefix[ p ]) { |
196 | type = type.toLowerCase() | 202 | type = type.toLowerCase() |
197 | } | 203 | } |
198 | 204 | ||
199 | if (action === 'addEvent') { | 205 | if (action === 'addEvent') { |
200 | element.addEventListener(prefix[p] + type, callback, false) | 206 | element.addEventListener(prefix[ p ] + type, callback, false) |
201 | } else if (action === 'removeEvent') { | 207 | } else if (action === 'removeEvent') { |
202 | element.removeEventListener(prefix[p] + type, callback, false) | 208 | element.removeEventListener(prefix[ p ] + type, callback, false) |
203 | } | 209 | } |
204 | } | 210 | } |
205 | } | 211 | } |
@@ -211,7 +217,7 @@ class SettingsMenuItem extends MenuItem { | |||
211 | 217 | ||
212 | if (this.menuToLoad === 'mainmenu') { | 218 | if (this.menuToLoad === 'mainmenu') { |
213 | // hide submenu | 219 | // hide submenu |
214 | videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | 220 | videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') |
215 | 221 | ||
216 | // reset opacity to 0 | 222 | // reset opacity to 0 |
217 | this.settingsSubMenuEl_.style.opacity = '0' | 223 | this.settingsSubMenuEl_.style.opacity = '0' |
@@ -219,25 +225,27 @@ class SettingsMenuItem extends MenuItem { | |||
219 | } | 225 | } |
220 | 226 | ||
221 | reset () { | 227 | reset () { |
222 | videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | 228 | videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') |
223 | this.settingsSubMenuEl_.style.opacity = '0' | 229 | this.settingsSubMenuEl_.style.opacity = '0' |
224 | this.setMargin() | 230 | this.setMargin() |
225 | } | 231 | } |
226 | 232 | ||
227 | loadMainMenu () { | 233 | loadMainMenu () { |
234 | const mainMenuEl = this.mainMenu.el() as HTMLElement | ||
228 | this.menuToLoad = 'mainmenu' | 235 | this.menuToLoad = 'mainmenu' |
229 | this.mainMenu.show() | 236 | this.mainMenu.show() |
230 | this.mainMenu.el_.style.opacity = '0' | 237 | mainMenuEl.style.opacity = '0' |
231 | 238 | ||
232 | // back button will always take you to main menu, so set dialog sizes | 239 | // back button will always take you to main menu, so set dialog sizes |
233 | this.settingsButton.setDialogSize([this.mainMenu.width, this.mainMenu.height]) | 240 | const mainMenuAny = this.mainMenu as any |
241 | this.settingsButton.setDialogSize([ mainMenuAny.width, mainMenuAny.height ]) | ||
234 | 242 | ||
235 | // animation not triggered without timeout (some async stuff ?!?) | 243 | // animation not triggered without timeout (some async stuff ?!?) |
236 | setTimeout(() => { | 244 | setTimeout(() => { |
237 | // animate margin and opacity before hiding the submenu | 245 | // animate margin and opacity before hiding the submenu |
238 | // this triggers CSS Transition event | 246 | // this triggers CSS Transition event |
239 | this.setMargin() | 247 | this.setMargin() |
240 | this.mainMenu.el_.style.opacity = '1' | 248 | mainMenuEl.style.opacity = '1' |
241 | }, 0) | 249 | }, 0) |
242 | } | 250 | } |
243 | 251 | ||
@@ -251,8 +259,8 @@ class SettingsMenuItem extends MenuItem { | |||
251 | this.update() | 259 | this.update() |
252 | }) | 260 | }) |
253 | 261 | ||
254 | this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_) | 262 | this.settingsSubMenuTitleEl_.innerHTML = this.player().localize(this.subMenu.controlText()) |
255 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) | 263 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el()) |
256 | this.panelChildEl.appendChild(this.settingsSubMenuEl_) | 264 | this.panelChildEl.appendChild(this.settingsSubMenuEl_) |
257 | this.update() | 265 | this.update() |
258 | 266 | ||
@@ -283,7 +291,8 @@ class SettingsMenuItem extends MenuItem { | |||
283 | // or sets options_['selected'] on the selected playback rate. | 291 | // or sets options_['selected'] on the selected playback rate. |
284 | // Thus we get the submenu value based on the labelEl of playbackRateMenuButton | 292 | // Thus we get the submenu value based on the labelEl of playbackRateMenuButton |
285 | if (subMenu === 'PlaybackRateMenuButton') { | 293 | if (subMenu === 'PlaybackRateMenuButton') { |
286 | setTimeout(() => this.settingsSubMenuValueEl_.innerHTML = this.subMenu.labelEl_.innerHTML, 250) | 294 | const html = (this.subMenu as any).labelEl_.innerHTML |
295 | setTimeout(() => this.settingsSubMenuValueEl_.innerHTML = html, 250) | ||
287 | } else { | 296 | } else { |
288 | // Loop trough the submenu items to find the selected child | 297 | // Loop trough the submenu items to find the selected child |
289 | for (const subMenuItem of this.subMenu.menu.children_) { | 298 | for (const subMenuItem of this.subMenu.menu.children_) { |
@@ -292,13 +301,15 @@ class SettingsMenuItem extends MenuItem { | |||
292 | } | 301 | } |
293 | 302 | ||
294 | if (subMenuItem.hasClass('vjs-selected')) { | 303 | if (subMenuItem.hasClass('vjs-selected')) { |
304 | const subMenuItemUntyped = subMenuItem as any | ||
305 | |||
295 | // Prefer to use the function | 306 | // Prefer to use the function |
296 | if (typeof subMenuItem.getLabel === 'function') { | 307 | if (typeof subMenuItemUntyped.getLabel === 'function') { |
297 | this.settingsSubMenuValueEl_.innerHTML = subMenuItem.getLabel() | 308 | this.settingsSubMenuValueEl_.innerHTML = subMenuItemUntyped.getLabel() |
298 | break | 309 | break |
299 | } | 310 | } |
300 | 311 | ||
301 | this.settingsSubMenuValueEl_.innerHTML = subMenuItem.options_.label | 312 | this.settingsSubMenuValueEl_.innerHTML = subMenuItemUntyped.options_.label |
302 | } | 313 | } |
303 | } | 314 | } |
304 | } | 315 | } |
@@ -313,7 +324,7 @@ class SettingsMenuItem extends MenuItem { | |||
313 | if (!(item instanceof component)) { | 324 | if (!(item instanceof component)) { |
314 | continue | 325 | continue |
315 | } | 326 | } |
316 | item.on(['tap', 'click'], this.submenuClickHandler) | 327 | item.on([ 'tap', 'click' ], this.submenuClickHandler) |
317 | } | 328 | } |
318 | } | 329 | } |
319 | 330 | ||
@@ -321,11 +332,11 @@ class SettingsMenuItem extends MenuItem { | |||
321 | // if number of submenu items change dynamically more logic will be needed | 332 | // if number of submenu items change dynamically more logic will be needed |
322 | setSize () { | 333 | setSize () { |
323 | this.dialog.removeClass('vjs-hidden') | 334 | this.dialog.removeClass('vjs-hidden') |
324 | videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') | 335 | videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') |
325 | this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) | 336 | this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) |
326 | this.setMargin() | 337 | this.setMargin() |
327 | this.dialog.addClass('vjs-hidden') | 338 | this.dialog.addClass('vjs-hidden') |
328 | videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | 339 | videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') |
329 | } | 340 | } |
330 | 341 | ||
331 | setMargin () { | 342 | setMargin () { |
@@ -341,19 +352,19 @@ class SettingsMenuItem extends MenuItem { | |||
341 | */ | 352 | */ |
342 | hideSubMenu () { | 353 | hideSubMenu () { |
343 | // after removing settings item this.el_ === null | 354 | // after removing settings item this.el_ === null |
344 | if (!this.el_) { | 355 | if (!this.el()) { |
345 | return | 356 | return |
346 | } | 357 | } |
347 | 358 | ||
348 | if (videojsUntyped.dom.hasClass(this.el_, 'open')) { | 359 | if (videojs.dom.hasClass(this.el(), 'open')) { |
349 | videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') | 360 | videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') |
350 | videojsUntyped.dom.removeClass(this.el_, 'open') | 361 | videojs.dom.removeClass(this.el(), 'open') |
351 | } | 362 | } |
352 | } | 363 | } |
353 | 364 | ||
354 | } | 365 | } |
355 | 366 | ||
356 | SettingsMenuItem.prototype.contentElType = 'button' | 367 | (SettingsMenuItem as any).prototype.contentElType = 'button' |
357 | videojsUntyped.registerComponent('SettingsMenuItem', SettingsMenuItem) | 368 | videojs.registerComponent('SettingsMenuItem', SettingsMenuItem) |
358 | 369 | ||
359 | export { SettingsMenuItem } | 370 | export { SettingsMenuItem } |
diff --git a/client/src/assets/player/videojs-components/settings-panel-child.ts b/client/src/assets/player/videojs-components/settings-panel-child.ts new file mode 100644 index 000000000..d12e8218a --- /dev/null +++ b/client/src/assets/player/videojs-components/settings-panel-child.ts | |||
@@ -0,0 +1,22 @@ | |||
1 | import videojs, { VideoJsPlayer } from 'video.js' | ||
2 | |||
3 | const Component = videojs.getComponent('Component') | ||
4 | |||
5 | class SettingsPanelChild extends Component { | ||
6 | |||
7 | constructor (player: VideoJsPlayer, options?: videojs.ComponentOptions) { | ||
8 | super(player, options) | ||
9 | } | ||
10 | |||
11 | createEl () { | ||
12 | return super.createEl('div', { | ||
13 | className: 'vjs-settings-panel-child', | ||
14 | innerHTML: '', | ||
15 | tabIndex: -1 | ||
16 | }) | ||
17 | } | ||
18 | } | ||
19 | |||
20 | Component.registerComponent('SettingsPanelChild', SettingsPanelChild) | ||
21 | |||
22 | export { SettingsPanelChild } | ||
diff --git a/client/src/assets/player/videojs-components/settings-panel.ts b/client/src/assets/player/videojs-components/settings-panel.ts new file mode 100644 index 000000000..2090abf45 --- /dev/null +++ b/client/src/assets/player/videojs-components/settings-panel.ts | |||
@@ -0,0 +1,22 @@ | |||
1 | import videojs, { VideoJsPlayer } from 'video.js' | ||
2 | |||
3 | const Component = videojs.getComponent('Component') | ||
4 | |||
5 | class SettingsPanel extends Component { | ||
6 | |||
7 | constructor (player: VideoJsPlayer, options?: videojs.ComponentOptions) { | ||
8 | super(player, options) | ||
9 | } | ||
10 | |||
11 | createEl () { | ||
12 | return super.createEl('div', { | ||
13 | className: 'vjs-settings-panel', | ||
14 | innerHTML: '', | ||
15 | tabIndex: -1 | ||
16 | }) | ||
17 | } | ||
18 | } | ||
19 | |||
20 | Component.registerComponent('SettingsPanel', SettingsPanel) | ||
21 | |||
22 | export { SettingsPanel } | ||
diff --git a/client/src/assets/player/videojs-components/theater-button.ts b/client/src/assets/player/videojs-components/theater-button.ts index bf383cf34..1c8c9f154 100644 --- a/client/src/assets/player/videojs-components/theater-button.ts +++ b/client/src/assets/player/videojs-components/theater-button.ts | |||
@@ -1,26 +1,24 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
4 | |||
5 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | ||
6 | import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage' | 2 | import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage' |
7 | 3 | ||
8 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 4 | const Button = videojs.getComponent('Button') |
9 | class TheaterButton extends Button { | 5 | class TheaterButton extends Button { |
10 | 6 | ||
11 | private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled' | 7 | private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled' |
12 | 8 | ||
13 | constructor (player: videojs.Player, options: any) { | 9 | constructor (player: VideoJsPlayer, options: videojs.ComponentOptions) { |
14 | super(player, options) | 10 | super(player, options) |
15 | 11 | ||
16 | const enabled = getStoredTheater() | 12 | const enabled = getStoredTheater() |
17 | if (enabled === true) { | 13 | if (enabled === true) { |
18 | this.player_.addClass(TheaterButton.THEATER_MODE_CLASS) | 14 | this.player().addClass(TheaterButton.THEATER_MODE_CLASS) |
19 | 15 | ||
20 | this.handleTheaterChange() | 16 | this.handleTheaterChange() |
21 | } | 17 | } |
22 | 18 | ||
23 | this.player_.theaterEnabled = enabled | 19 | this.controlText('Theater mode') |
20 | |||
21 | this.player().theaterEnabled = enabled | ||
24 | } | 22 | } |
25 | 23 | ||
26 | buildCSSClass () { | 24 | buildCSSClass () { |
@@ -52,6 +50,4 @@ class TheaterButton extends Button { | |||
52 | } | 50 | } |
53 | } | 51 | } |
54 | 52 | ||
55 | TheaterButton.prototype.controlText_ = 'Theater mode' | 53 | videojs.registerComponent('TheaterButton', TheaterButton) |
56 | |||
57 | TheaterButton.registerComponent('TheaterButton', TheaterButton) | ||
diff --git a/client/src/assets/player/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts index 35cf85c99..bf6b0a718 100644 --- a/client/src/assets/player/webtorrent/webtorrent-plugin.ts +++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts | |||
@@ -1,17 +1,15 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | 1 | import videojs, { VideoJsPlayer } from 'video.js' |
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
4 | 2 | ||
5 | import * as WebTorrent from 'webtorrent' | 3 | import * as WebTorrent from 'webtorrent' |
6 | import { renderVideo } from './video-renderer' | 4 | import { renderVideo } from './video-renderer' |
7 | import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from '../peertube-videojs-typings' | 5 | import { LoadedQualityData, PlayerNetworkInfo, WebtorrentPluginOptions } from '../peertube-videojs-typings' |
8 | import { getRtcConfig, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from '../utils' | 6 | import { getRtcConfig, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from '../utils' |
9 | import { PeertubeChunkStore } from './peertube-chunk-store' | 7 | import { PeertubeChunkStore } from './peertube-chunk-store' |
10 | import { | 8 | import { |
11 | getAverageBandwidthInStore, | 9 | getAverageBandwidthInStore, |
12 | getStoredMute, | 10 | getStoredMute, |
13 | getStoredVolume, | ||
14 | getStoredP2PEnabled, | 11 | getStoredP2PEnabled, |
12 | getStoredVolume, | ||
15 | saveAverageBandwidth | 13 | saveAverageBandwidth |
16 | } from '../peertube-player-local-storage' | 14 | } from '../peertube-player-local-storage' |
17 | import { VideoFile } from '@shared/models' | 15 | import { VideoFile } from '@shared/models' |
@@ -24,14 +22,16 @@ type PlayOptions = { | |||
24 | delay?: number | 22 | delay?: number |
25 | } | 23 | } |
26 | 24 | ||
27 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | 25 | const Plugin = videojs.getPlugin('plugin') |
26 | |||
28 | class WebTorrentPlugin extends Plugin { | 27 | class WebTorrentPlugin extends Plugin { |
28 | readonly videoFiles: VideoFile[] | ||
29 | |||
29 | private readonly playerElement: HTMLVideoElement | 30 | private readonly playerElement: HTMLVideoElement |
30 | 31 | ||
31 | private readonly autoplay: boolean = false | 32 | private readonly autoplay: boolean = false |
32 | private readonly startTime: number = 0 | 33 | private readonly startTime: number = 0 |
33 | private readonly savePlayerSrcFunction: Function | 34 | private readonly savePlayerSrcFunction: VideoJsPlayer['src'] |
34 | private readonly videoFiles: VideoFile[] | ||
35 | private readonly videoDuration: number | 35 | private readonly videoDuration: number |
36 | private readonly CONSTANTS = { | 36 | private readonly CONSTANTS = { |
37 | INFO_SCHEDULER: 1000, // Don't change this | 37 | INFO_SCHEDULER: 1000, // Don't change this |
@@ -49,7 +49,6 @@ class WebTorrentPlugin extends Plugin { | |||
49 | dht: false | 49 | dht: false |
50 | }) | 50 | }) |
51 | 51 | ||
52 | private player: any | ||
53 | private currentVideoFile: VideoFile | 52 | private currentVideoFile: VideoFile |
54 | private torrent: WebTorrent.Torrent | 53 | private torrent: WebTorrent.Torrent |
55 | 54 | ||
@@ -70,8 +69,8 @@ class WebTorrentPlugin extends Plugin { | |||
70 | 69 | ||
71 | private downloadSpeeds: number[] = [] | 70 | private downloadSpeeds: number[] = [] |
72 | 71 | ||
73 | constructor (player: videojs.Player, options: WebtorrentPluginOptions) { | 72 | constructor (player: VideoJsPlayer, options?: WebtorrentPluginOptions) { |
74 | super(player, options) | 73 | super(player) |
75 | 74 | ||
76 | this.startTime = timeToInt(options.startTime) | 75 | this.startTime = timeToInt(options.startTime) |
77 | 76 | ||
@@ -147,12 +146,12 @@ class WebTorrentPlugin extends Plugin { | |||
147 | } | 146 | } |
148 | 147 | ||
149 | // Do not display error to user because we will have multiple fallback | 148 | // Do not display error to user because we will have multiple fallback |
150 | this.disableErrorDisplay() | 149 | this.disableErrorDisplay(); |
151 | 150 | ||
152 | // Hack to "simulate" src link in video.js >= 6 | 151 | // Hack to "simulate" src link in video.js >= 6 |
153 | // Without this, we can't play the video after pausing it | 152 | // Without this, we can't play the video after pausing it |
154 | // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633 | 153 | // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633 |
155 | this.player.src = () => true | 154 | (this.player as any).src = () => true |
156 | const oldPlaybackRate = this.player.playbackRate() | 155 | const oldPlaybackRate = this.player.playbackRate() |
157 | 156 | ||
158 | const previousVideoFile = this.currentVideoFile | 157 | const previousVideoFile = this.currentVideoFile |
@@ -333,7 +332,7 @@ class WebTorrentPlugin extends Plugin { | |||
333 | 332 | ||
334 | const playPromise = this.player.play() | 333 | const playPromise = this.player.play() |
335 | if (playPromise !== undefined) { | 334 | if (playPromise !== undefined) { |
336 | return playPromise.then(done) | 335 | return playPromise.then(() => done()) |
337 | .catch((err: Error) => { | 336 | .catch((err: Error) => { |
338 | if (err.message.indexOf('The play() request was interrupted by a call to pause()') !== -1) { | 337 | if (err.message.indexOf('The play() request was interrupted by a call to pause()') !== -1) { |
339 | return | 338 | return |
@@ -426,8 +425,8 @@ class WebTorrentPlugin extends Plugin { | |||
426 | } | 425 | } |
427 | 426 | ||
428 | // Proxy first play | 427 | // Proxy first play |
429 | const oldPlay = this.player.play.bind(this.player) | 428 | const oldPlay = this.player.play.bind(this.player); |
430 | this.player.play = () => { | 429 | (this.player as any).play = () => { |
431 | this.player.addClass('vjs-has-big-play-button-clicked') | 430 | this.player.addClass('vjs-has-big-play-button-clicked') |
432 | this.player.play = oldPlay | 431 | this.player.play = oldPlay |
433 | 432 | ||
@@ -619,7 +618,7 @@ class WebTorrentPlugin extends Plugin { | |||
619 | video: qualityLevelsPayload | 618 | video: qualityLevelsPayload |
620 | } | 619 | } |
621 | } | 620 | } |
622 | this.player.tech_.trigger('loadedqualitydata', payload) | 621 | this.player.tech(true).trigger('loadedqualitydata', payload) |
623 | } | 622 | } |
624 | 623 | ||
625 | private buildQualityLabel (file: VideoFile) { | 624 | private buildQualityLabel (file: VideoFile) { |
@@ -651,9 +650,9 @@ class WebTorrentPlugin extends Plugin { | |||
651 | return | 650 | return |
652 | } | 651 | } |
653 | 652 | ||
654 | for (let i = 0; i < qualityLevels; i++) { | 653 | for (let i = 0; i < qualityLevels.length; i++) { |
655 | const q = this.player.qualityLevels[i] | 654 | const q = qualityLevels[i] |
656 | if (q.height === resolutionId) qualityLevels.selectedIndex = i | 655 | if (q.height === resolutionId) qualityLevels.selectedIndex_ = i |
657 | } | 656 | } |
658 | } | 657 | } |
659 | } | 658 | } |