diff options
Diffstat (limited to 'client/src/assets/player')
17 files changed, 915 insertions, 627 deletions
diff --git a/client/src/assets/player/p2p-media-loader-plugin.ts b/client/src/assets/player/p2p-media-loader-plugin.ts new file mode 100644 index 000000000..6d07a2c9c --- /dev/null +++ b/client/src/assets/player/p2p-media-loader-plugin.ts | |||
@@ -0,0 +1,33 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
4 | import { P2PMediaLoaderPluginOptions, VideoJSComponentInterface } from './peertube-videojs-typings' | ||
5 | |||
6 | // videojs-hlsjs-plugin needs videojs in window | ||
7 | window['videojs'] = videojs | ||
8 | import '@streamroot/videojs-hlsjs-plugin' | ||
9 | |||
10 | import { initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs' | ||
11 | |||
12 | // import { Events } from '../p2p-media-loader/p2p-media-loader-core/lib' | ||
13 | |||
14 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | ||
15 | class P2pMediaLoaderPlugin extends Plugin { | ||
16 | |||
17 | constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) { | ||
18 | super(player, options) | ||
19 | |||
20 | initVideoJsContribHlsJsPlayer(player) | ||
21 | |||
22 | console.log(options) | ||
23 | |||
24 | player.src({ | ||
25 | type: options.type, | ||
26 | src: options.src | ||
27 | }) | ||
28 | } | ||
29 | |||
30 | } | ||
31 | |||
32 | videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin) | ||
33 | export { P2pMediaLoaderPlugin } | ||
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts new file mode 100644 index 000000000..9155c0698 --- /dev/null +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -0,0 +1,388 @@ | |||
1 | import { VideoFile } from '../../../../shared/models/videos' | ||
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
4 | import 'videojs-hotkeys' | ||
5 | import 'videojs-dock' | ||
6 | import 'videojs-contextmenu-ui' | ||
7 | import 'videojs-contrib-quality-levels' | ||
8 | import './peertube-plugin' | ||
9 | import './videojs-components/peertube-link-button' | ||
10 | import './videojs-components/resolution-menu-button' | ||
11 | import './videojs-components/settings-menu-button' | ||
12 | import './videojs-components/p2p-info-button' | ||
13 | import './videojs-components/peertube-load-progress-bar' | ||
14 | import './videojs-components/theater-button' | ||
15 | import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions, videojsUntyped } from './peertube-videojs-typings' | ||
16 | import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils' | ||
17 | import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n' | ||
18 | import { Engine } from 'p2p-media-loader-hlsjs' | ||
19 | |||
20 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | ||
21 | videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' | ||
22 | // Change Captions to Subtitles/CC | ||
23 | videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC' | ||
24 | // We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) | ||
25 | videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' ' | ||
26 | |||
27 | type PlayerMode = 'webtorrent' | 'p2p-media-loader' | ||
28 | |||
29 | type WebtorrentOptions = { | ||
30 | videoFiles: VideoFile[] | ||
31 | } | ||
32 | |||
33 | type P2PMediaLoaderOptions = { | ||
34 | playlistUrl: string | ||
35 | } | ||
36 | |||
37 | type CommonOptions = { | ||
38 | playerElement: HTMLVideoElement | ||
39 | |||
40 | autoplay: boolean | ||
41 | videoDuration: number | ||
42 | enableHotkeys: boolean | ||
43 | inactivityTimeout: number | ||
44 | poster: string | ||
45 | startTime: number | string | ||
46 | |||
47 | theaterMode: boolean | ||
48 | captions: boolean | ||
49 | peertubeLink: boolean | ||
50 | |||
51 | videoViewUrl: string | ||
52 | embedUrl: string | ||
53 | |||
54 | language?: string | ||
55 | controls?: boolean | ||
56 | muted?: boolean | ||
57 | loop?: boolean | ||
58 | subtitle?: string | ||
59 | |||
60 | videoCaptions: VideoJSCaption[] | ||
61 | |||
62 | userWatching?: UserWatching | ||
63 | |||
64 | serverUrl: string | ||
65 | } | ||
66 | |||
67 | export type PeertubePlayerManagerOptions = { | ||
68 | common: CommonOptions, | ||
69 | webtorrent?: WebtorrentOptions, | ||
70 | p2pMediaLoader?: P2PMediaLoaderOptions | ||
71 | } | ||
72 | |||
73 | export class PeertubePlayerManager { | ||
74 | |||
75 | private static videojsLocaleCache: { [ path: string ]: any } = {} | ||
76 | |||
77 | static getServerTranslations (serverUrl: string, locale: string) { | ||
78 | const path = PeertubePlayerManager.getLocalePath(serverUrl, locale) | ||
79 | // It is the default locale, nothing to translate | ||
80 | if (!path) return Promise.resolve(undefined) | ||
81 | |||
82 | return fetch(path + '/server.json') | ||
83 | .then(res => res.json()) | ||
84 | .catch(err => { | ||
85 | console.error('Cannot get server translations', err) | ||
86 | return undefined | ||
87 | }) | ||
88 | } | ||
89 | |||
90 | static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions) { | ||
91 | if (mode === 'webtorrent') await import('./webtorrent-plugin') | ||
92 | if (mode === 'p2p-media-loader') await import('./p2p-media-loader-plugin') | ||
93 | |||
94 | const videojsOptions = this.getVideojsOptions(mode, options) | ||
95 | |||
96 | await this.loadLocaleInVideoJS(options.common.serverUrl, options.common.language) | ||
97 | |||
98 | const self = this | ||
99 | return new Promise(res => { | ||
100 | videojs(options.common.playerElement, videojsOptions, function (this: any) { | ||
101 | const player = this | ||
102 | |||
103 | self.addContextMenu(mode, player, options.common.embedUrl) | ||
104 | |||
105 | return res(player) | ||
106 | }) | ||
107 | }) | ||
108 | } | ||
109 | |||
110 | private static loadLocaleInVideoJS (serverUrl: string, locale: string) { | ||
111 | const path = PeertubePlayerManager.getLocalePath(serverUrl, locale) | ||
112 | // It is the default locale, nothing to translate | ||
113 | if (!path) return Promise.resolve(undefined) | ||
114 | |||
115 | let p: Promise<any> | ||
116 | |||
117 | if (PeertubePlayerManager.videojsLocaleCache[path]) { | ||
118 | p = Promise.resolve(PeertubePlayerManager.videojsLocaleCache[path]) | ||
119 | } else { | ||
120 | p = fetch(path + '/player.json') | ||
121 | .then(res => res.json()) | ||
122 | .then(json => { | ||
123 | PeertubePlayerManager.videojsLocaleCache[path] = json | ||
124 | return json | ||
125 | }) | ||
126 | .catch(err => { | ||
127 | console.error('Cannot get player translations', err) | ||
128 | return undefined | ||
129 | }) | ||
130 | } | ||
131 | |||
132 | const completeLocale = getCompleteLocale(locale) | ||
133 | return p.then(json => videojs.addLanguage(getShortLocale(completeLocale), json)) | ||
134 | } | ||
135 | |||
136 | private static getVideojsOptions (mode: PlayerMode, options: PeertubePlayerManagerOptions) { | ||
137 | const commonOptions = options.common | ||
138 | const webtorrentOptions = options.webtorrent | ||
139 | const p2pMediaLoaderOptions = options.p2pMediaLoader | ||
140 | |||
141 | const plugins: VideoJSPluginOptions = { | ||
142 | peertube: { | ||
143 | autoplay: commonOptions.autoplay, // Use peertube plugin autoplay because we get the file by webtorrent | ||
144 | videoViewUrl: commonOptions.videoViewUrl, | ||
145 | videoDuration: commonOptions.videoDuration, | ||
146 | startTime: commonOptions.startTime, | ||
147 | userWatching: commonOptions.userWatching, | ||
148 | subtitle: commonOptions.subtitle, | ||
149 | videoCaptions: commonOptions.videoCaptions | ||
150 | } | ||
151 | } | ||
152 | |||
153 | if (p2pMediaLoaderOptions) { | ||
154 | const p2pMediaLoader: P2PMediaLoaderPluginOptions = { | ||
155 | type: 'application/x-mpegURL', | ||
156 | src: p2pMediaLoaderOptions.playlistUrl | ||
157 | } | ||
158 | |||
159 | const config = { | ||
160 | segments: { | ||
161 | swarmId: 'swarm' // TODO: choose swarm id | ||
162 | } | ||
163 | } | ||
164 | const streamrootHls = { | ||
165 | html5: { | ||
166 | hlsjsConfig: { | ||
167 | liveSyncDurationCount: 7, | ||
168 | loader: new Engine(config).createLoaderClass() | ||
169 | } | ||
170 | } | ||
171 | } | ||
172 | |||
173 | Object.assign(plugins, { p2pMediaLoader, streamrootHls }) | ||
174 | } | ||
175 | |||
176 | if (webtorrentOptions) { | ||
177 | const webtorrent = { | ||
178 | autoplay: commonOptions.autoplay, | ||
179 | videoDuration: commonOptions.videoDuration, | ||
180 | playerElement: commonOptions.playerElement, | ||
181 | videoFiles: webtorrentOptions.videoFiles | ||
182 | } | ||
183 | Object.assign(plugins, { webtorrent }) | ||
184 | } | ||
185 | |||
186 | const videojsOptions = { | ||
187 | // We don't use text track settings for now | ||
188 | textTrackSettings: false, | ||
189 | controls: commonOptions.controls !== undefined ? commonOptions.controls : true, | ||
190 | loop: commonOptions.loop !== undefined ? commonOptions.loop : false, | ||
191 | |||
192 | muted: commonOptions.muted !== undefined | ||
193 | ? commonOptions.muted | ||
194 | : undefined, // Undefined so the player knows it has to check the local storage | ||
195 | |||
196 | poster: commonOptions.poster, | ||
197 | autoplay: false, | ||
198 | inactivityTimeout: commonOptions.inactivityTimeout, | ||
199 | playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ], | ||
200 | plugins, | ||
201 | controlBar: { | ||
202 | children: this.getControlBarChildren(mode, { | ||
203 | captions: commonOptions.captions, | ||
204 | peertubeLink: commonOptions.peertubeLink, | ||
205 | theaterMode: commonOptions.theaterMode | ||
206 | }) | ||
207 | } | ||
208 | } | ||
209 | |||
210 | if (commonOptions.enableHotkeys === true) { | ||
211 | Object.assign(videojsOptions.plugins, { | ||
212 | hotkeys: { | ||
213 | enableVolumeScroll: false, | ||
214 | enableModifiersForNumbers: false, | ||
215 | |||
216 | fullscreenKey: function (event: KeyboardEvent) { | ||
217 | // fullscreen with the f key or Ctrl+Enter | ||
218 | return event.key === 'f' || (event.ctrlKey && event.key === 'Enter') | ||
219 | }, | ||
220 | |||
221 | seekStep: function (event: KeyboardEvent) { | ||
222 | // mimic VLC seek behavior, and default to 5 (original value is 5). | ||
223 | if (event.ctrlKey && event.altKey) { | ||
224 | return 5 * 60 | ||
225 | } else if (event.ctrlKey) { | ||
226 | return 60 | ||
227 | } else if (event.altKey) { | ||
228 | return 10 | ||
229 | } else { | ||
230 | return 5 | ||
231 | } | ||
232 | }, | ||
233 | |||
234 | customKeys: { | ||
235 | increasePlaybackRateKey: { | ||
236 | key: function (event: KeyboardEvent) { | ||
237 | return event.key === '>' | ||
238 | }, | ||
239 | handler: function (player: videojs.Player) { | ||
240 | player.playbackRate((player.playbackRate() + 0.1).toFixed(2)) | ||
241 | } | ||
242 | }, | ||
243 | decreasePlaybackRateKey: { | ||
244 | key: function (event: KeyboardEvent) { | ||
245 | return event.key === '<' | ||
246 | }, | ||
247 | handler: function (player: videojs.Player) { | ||
248 | player.playbackRate((player.playbackRate() - 0.1).toFixed(2)) | ||
249 | } | ||
250 | }, | ||
251 | frameByFrame: { | ||
252 | key: function (event: KeyboardEvent) { | ||
253 | return event.key === '.' | ||
254 | }, | ||
255 | handler: function (player: videojs.Player) { | ||
256 | player.pause() | ||
257 | // Calculate movement distance (assuming 30 fps) | ||
258 | const dist = 1 / 30 | ||
259 | player.currentTime(player.currentTime() + dist) | ||
260 | } | ||
261 | } | ||
262 | } | ||
263 | } | ||
264 | }) | ||
265 | } | ||
266 | |||
267 | if (commonOptions.language && !isDefaultLocale(commonOptions.language)) { | ||
268 | Object.assign(videojsOptions, { language: commonOptions.language }) | ||
269 | } | ||
270 | |||
271 | return videojsOptions | ||
272 | } | ||
273 | |||
274 | private static getControlBarChildren (mode: PlayerMode, options: { | ||
275 | peertubeLink: boolean | ||
276 | theaterMode: boolean, | ||
277 | captions: boolean | ||
278 | }) { | ||
279 | const settingEntries = [] | ||
280 | const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar' | ||
281 | |||
282 | // Keep an order | ||
283 | settingEntries.push('playbackRateMenuButton') | ||
284 | if (options.captions === true) settingEntries.push('captionsButton') | ||
285 | settingEntries.push('resolutionMenuButton') | ||
286 | |||
287 | const children = { | ||
288 | 'playToggle': {}, | ||
289 | 'currentTimeDisplay': {}, | ||
290 | 'timeDivider': {}, | ||
291 | 'durationDisplay': {}, | ||
292 | 'liveDisplay': {}, | ||
293 | |||
294 | 'flexibleWidthSpacer': {}, | ||
295 | 'progressControl': { | ||
296 | children: { | ||
297 | 'seekBar': { | ||
298 | children: { | ||
299 | [loadProgressBar]: {}, | ||
300 | 'mouseTimeDisplay': {}, | ||
301 | 'playProgressBar': {} | ||
302 | } | ||
303 | } | ||
304 | } | ||
305 | }, | ||
306 | |||
307 | 'p2PInfoButton': {}, | ||
308 | |||
309 | 'muteToggle': {}, | ||
310 | 'volumeControl': {}, | ||
311 | |||
312 | 'settingsButton': { | ||
313 | setup: { | ||
314 | maxHeightOffset: 40 | ||
315 | }, | ||
316 | entries: settingEntries | ||
317 | } | ||
318 | } | ||
319 | |||
320 | if (options.peertubeLink === true) { | ||
321 | Object.assign(children, { | ||
322 | 'peerTubeLinkButton': {} | ||
323 | }) | ||
324 | } | ||
325 | |||
326 | if (options.theaterMode === true) { | ||
327 | Object.assign(children, { | ||
328 | 'theaterButton': {} | ||
329 | }) | ||
330 | } | ||
331 | |||
332 | Object.assign(children, { | ||
333 | 'fullscreenToggle': {} | ||
334 | }) | ||
335 | |||
336 | return children | ||
337 | } | ||
338 | |||
339 | private static addContextMenu (mode: PlayerMode, player: any, videoEmbedUrl: string) { | ||
340 | const content = [ | ||
341 | { | ||
342 | label: player.localize('Copy the video URL'), | ||
343 | listener: function () { | ||
344 | copyToClipboard(buildVideoLink()) | ||
345 | } | ||
346 | }, | ||
347 | { | ||
348 | label: player.localize('Copy the video URL at the current time'), | ||
349 | listener: function () { | ||
350 | const player = this as videojs.Player | ||
351 | copyToClipboard(buildVideoLink(player.currentTime())) | ||
352 | } | ||
353 | }, | ||
354 | { | ||
355 | label: player.localize('Copy embed code'), | ||
356 | listener: () => { | ||
357 | copyToClipboard(buildVideoEmbed(videoEmbedUrl)) | ||
358 | } | ||
359 | } | ||
360 | ] | ||
361 | |||
362 | if (mode === 'webtorrent') { | ||
363 | content.push({ | ||
364 | label: player.localize('Copy magnet URI'), | ||
365 | listener: function () { | ||
366 | const player = this as videojs.Player | ||
367 | copyToClipboard(player.webtorrent().getCurrentVideoFile().magnetUri) | ||
368 | } | ||
369 | }) | ||
370 | } | ||
371 | |||
372 | player.contextmenuUI({ content }) | ||
373 | } | ||
374 | |||
375 | private static getLocalePath (serverUrl: string, locale: string) { | ||
376 | const completeLocale = getCompleteLocale(locale) | ||
377 | |||
378 | if (!is18nLocale(completeLocale) || isDefaultLocale(completeLocale)) return undefined | ||
379 | |||
380 | return serverUrl + '/client/locales/' + completeLocale | ||
381 | } | ||
382 | } | ||
383 | |||
384 | // ############################################################################ | ||
385 | |||
386 | export { | ||
387 | videojs | ||
388 | } | ||
diff --git a/client/src/assets/player/peertube-player.ts b/client/src/assets/player/peertube-player.ts deleted file mode 100644 index 2de6d7fef..000000000 --- a/client/src/assets/player/peertube-player.ts +++ /dev/null | |||
@@ -1,300 +0,0 @@ | |||
1 | import { VideoFile } from '../../../../shared/models/videos' | ||
2 | |||
3 | import 'videojs-hotkeys' | ||
4 | import 'videojs-dock' | ||
5 | import 'videojs-contextmenu-ui' | ||
6 | import './peertube-link-button' | ||
7 | import './resolution-menu-button' | ||
8 | import './settings-menu-button' | ||
9 | import './webtorrent-info-button' | ||
10 | import './peertube-videojs-plugin' | ||
11 | import './peertube-load-progress-bar' | ||
12 | import './theater-button' | ||
13 | import { UserWatching, VideoJSCaption, videojsUntyped } from './peertube-videojs-typings' | ||
14 | import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils' | ||
15 | import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n' | ||
16 | |||
17 | // FIXME: something weird with our path definition in tsconfig and typings | ||
18 | // @ts-ignore | ||
19 | import { Player } from 'video.js' | ||
20 | |||
21 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | ||
22 | videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' | ||
23 | // Change Captions to Subtitles/CC | ||
24 | videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC' | ||
25 | // We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) | ||
26 | videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' ' | ||
27 | |||
28 | function getVideojsOptions (options: { | ||
29 | autoplay: boolean | ||
30 | playerElement: HTMLVideoElement | ||
31 | videoViewUrl: string | ||
32 | videoDuration: number | ||
33 | videoFiles: VideoFile[] | ||
34 | enableHotkeys: boolean | ||
35 | inactivityTimeout: number | ||
36 | peertubeLink: boolean | ||
37 | poster: string | ||
38 | startTime: number | string | ||
39 | theaterMode: boolean | ||
40 | videoCaptions: VideoJSCaption[] | ||
41 | |||
42 | language?: string | ||
43 | controls?: boolean | ||
44 | muted?: boolean | ||
45 | loop?: boolean | ||
46 | subtitle?: string | ||
47 | |||
48 | userWatching?: UserWatching | ||
49 | }) { | ||
50 | const videojsOptions = { | ||
51 | // We don't use text track settings for now | ||
52 | textTrackSettings: false, | ||
53 | controls: options.controls !== undefined ? options.controls : true, | ||
54 | loop: options.loop !== undefined ? options.loop : false, | ||
55 | |||
56 | muted: options.muted !== undefined ? options.muted : undefined, // Undefined so the player knows it has to check the local storage | ||
57 | |||
58 | poster: options.poster, | ||
59 | autoplay: false, | ||
60 | inactivityTimeout: options.inactivityTimeout, | ||
61 | playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ], | ||
62 | plugins: { | ||
63 | peertube: { | ||
64 | autoplay: options.autoplay, // Use peertube plugin autoplay because we get the file by webtorrent | ||
65 | videoCaptions: options.videoCaptions, | ||
66 | videoFiles: options.videoFiles, | ||
67 | playerElement: options.playerElement, | ||
68 | videoViewUrl: options.videoViewUrl, | ||
69 | videoDuration: options.videoDuration, | ||
70 | startTime: options.startTime, | ||
71 | userWatching: options.userWatching, | ||
72 | subtitle: options.subtitle | ||
73 | } | ||
74 | }, | ||
75 | controlBar: { | ||
76 | children: getControlBarChildren(options) | ||
77 | } | ||
78 | } | ||
79 | |||
80 | if (options.enableHotkeys === true) { | ||
81 | Object.assign(videojsOptions.plugins, { | ||
82 | hotkeys: { | ||
83 | enableVolumeScroll: false, | ||
84 | enableModifiersForNumbers: false, | ||
85 | |||
86 | fullscreenKey: function (event: KeyboardEvent) { | ||
87 | // fullscreen with the f key or Ctrl+Enter | ||
88 | return event.key === 'f' || (event.ctrlKey && event.key === 'Enter') | ||
89 | }, | ||
90 | |||
91 | seekStep: function (event: KeyboardEvent) { | ||
92 | // mimic VLC seek behavior, and default to 5 (original value is 5). | ||
93 | if (event.ctrlKey && event.altKey) { | ||
94 | return 5 * 60 | ||
95 | } else if (event.ctrlKey) { | ||
96 | return 60 | ||
97 | } else if (event.altKey) { | ||
98 | return 10 | ||
99 | } else { | ||
100 | return 5 | ||
101 | } | ||
102 | }, | ||
103 | |||
104 | customKeys: { | ||
105 | increasePlaybackRateKey: { | ||
106 | key: function (event: KeyboardEvent) { | ||
107 | return event.key === '>' | ||
108 | }, | ||
109 | handler: function (player: Player) { | ||
110 | player.playbackRate((player.playbackRate() + 0.1).toFixed(2)) | ||
111 | } | ||
112 | }, | ||
113 | decreasePlaybackRateKey: { | ||
114 | key: function (event: KeyboardEvent) { | ||
115 | return event.key === '<' | ||
116 | }, | ||
117 | handler: function (player: Player) { | ||
118 | player.playbackRate((player.playbackRate() - 0.1).toFixed(2)) | ||
119 | } | ||
120 | }, | ||
121 | frameByFrame: { | ||
122 | key: function (event: KeyboardEvent) { | ||
123 | return event.key === '.' | ||
124 | }, | ||
125 | handler: function (player: Player) { | ||
126 | player.pause() | ||
127 | // Calculate movement distance (assuming 30 fps) | ||
128 | const dist = 1 / 30 | ||
129 | player.currentTime(player.currentTime() + dist) | ||
130 | } | ||
131 | } | ||
132 | } | ||
133 | } | ||
134 | }) | ||
135 | } | ||
136 | |||
137 | if (options.language && !isDefaultLocale(options.language)) { | ||
138 | Object.assign(videojsOptions, { language: options.language }) | ||
139 | } | ||
140 | |||
141 | return videojsOptions | ||
142 | } | ||
143 | |||
144 | function getControlBarChildren (options: { | ||
145 | peertubeLink: boolean | ||
146 | theaterMode: boolean, | ||
147 | videoCaptions: VideoJSCaption[] | ||
148 | }) { | ||
149 | const settingEntries = [] | ||
150 | |||
151 | // Keep an order | ||
152 | settingEntries.push('playbackRateMenuButton') | ||
153 | if (options.videoCaptions.length !== 0) settingEntries.push('captionsButton') | ||
154 | settingEntries.push('resolutionMenuButton') | ||
155 | |||
156 | const children = { | ||
157 | 'playToggle': {}, | ||
158 | 'currentTimeDisplay': {}, | ||
159 | 'timeDivider': {}, | ||
160 | 'durationDisplay': {}, | ||
161 | 'liveDisplay': {}, | ||
162 | |||
163 | 'flexibleWidthSpacer': {}, | ||
164 | 'progressControl': { | ||
165 | children: { | ||
166 | 'seekBar': { | ||
167 | children: { | ||
168 | 'peerTubeLoadProgressBar': {}, | ||
169 | 'mouseTimeDisplay': {}, | ||
170 | 'playProgressBar': {} | ||
171 | } | ||
172 | } | ||
173 | } | ||
174 | }, | ||
175 | |||
176 | 'webTorrentButton': {}, | ||
177 | |||
178 | 'muteToggle': {}, | ||
179 | 'volumeControl': {}, | ||
180 | |||
181 | 'settingsButton': { | ||
182 | setup: { | ||
183 | maxHeightOffset: 40 | ||
184 | }, | ||
185 | entries: settingEntries | ||
186 | } | ||
187 | } | ||
188 | |||
189 | if (options.peertubeLink === true) { | ||
190 | Object.assign(children, { | ||
191 | 'peerTubeLinkButton': {} | ||
192 | }) | ||
193 | } | ||
194 | |||
195 | if (options.theaterMode === true) { | ||
196 | Object.assign(children, { | ||
197 | 'theaterButton': {} | ||
198 | }) | ||
199 | } | ||
200 | |||
201 | Object.assign(children, { | ||
202 | 'fullscreenToggle': {} | ||
203 | }) | ||
204 | |||
205 | return children | ||
206 | } | ||
207 | |||
208 | function addContextMenu (player: any, videoEmbedUrl: string) { | ||
209 | player.contextmenuUI({ | ||
210 | content: [ | ||
211 | { | ||
212 | label: player.localize('Copy the video URL'), | ||
213 | listener: function () { | ||
214 | copyToClipboard(buildVideoLink()) | ||
215 | } | ||
216 | }, | ||
217 | { | ||
218 | label: player.localize('Copy the video URL at the current time'), | ||
219 | listener: function () { | ||
220 | const player = this as Player | ||
221 | copyToClipboard(buildVideoLink(player.currentTime())) | ||
222 | } | ||
223 | }, | ||
224 | { | ||
225 | label: player.localize('Copy embed code'), | ||
226 | listener: () => { | ||
227 | copyToClipboard(buildVideoEmbed(videoEmbedUrl)) | ||
228 | } | ||
229 | }, | ||
230 | { | ||
231 | label: player.localize('Copy magnet URI'), | ||
232 | listener: function () { | ||
233 | const player = this as Player | ||
234 | copyToClipboard(player.peertube().getCurrentVideoFile().magnetUri) | ||
235 | } | ||
236 | } | ||
237 | ] | ||
238 | }) | ||
239 | } | ||
240 | |||
241 | function loadLocaleInVideoJS (serverUrl: string, videojs: any, locale: string) { | ||
242 | const path = getLocalePath(serverUrl, locale) | ||
243 | // It is the default locale, nothing to translate | ||
244 | if (!path) return Promise.resolve(undefined) | ||
245 | |||
246 | let p: Promise<any> | ||
247 | |||
248 | if (loadLocaleInVideoJS.cache[path]) { | ||
249 | p = Promise.resolve(loadLocaleInVideoJS.cache[path]) | ||
250 | } else { | ||
251 | p = fetch(path + '/player.json') | ||
252 | .then(res => res.json()) | ||
253 | .then(json => { | ||
254 | loadLocaleInVideoJS.cache[path] = json | ||
255 | return json | ||
256 | }) | ||
257 | .catch(err => { | ||
258 | console.error('Cannot get player translations', err) | ||
259 | return undefined | ||
260 | }) | ||
261 | } | ||
262 | |||
263 | const completeLocale = getCompleteLocale(locale) | ||
264 | return p.then(json => videojs.addLanguage(getShortLocale(completeLocale), json)) | ||
265 | } | ||
266 | namespace loadLocaleInVideoJS { | ||
267 | export const cache: { [ path: string ]: any } = {} | ||
268 | } | ||
269 | |||
270 | function getServerTranslations (serverUrl: string, locale: string) { | ||
271 | const path = getLocalePath(serverUrl, locale) | ||
272 | // It is the default locale, nothing to translate | ||
273 | if (!path) return Promise.resolve(undefined) | ||
274 | |||
275 | return fetch(path + '/server.json') | ||
276 | .then(res => res.json()) | ||
277 | .catch(err => { | ||
278 | console.error('Cannot get server translations', err) | ||
279 | return undefined | ||
280 | }) | ||
281 | } | ||
282 | |||
283 | // ############################################################################ | ||
284 | |||
285 | export { | ||
286 | getServerTranslations, | ||
287 | loadLocaleInVideoJS, | ||
288 | getVideojsOptions, | ||
289 | addContextMenu | ||
290 | } | ||
291 | |||
292 | // ############################################################################ | ||
293 | |||
294 | function getLocalePath (serverUrl: string, locale: string) { | ||
295 | const completeLocale = getCompleteLocale(locale) | ||
296 | |||
297 | if (!is18nLocale(completeLocale) || isDefaultLocale(completeLocale)) return undefined | ||
298 | |||
299 | return serverUrl + '/client/locales/' + completeLocale | ||
300 | } | ||
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts new file mode 100644 index 000000000..0bd607697 --- /dev/null +++ b/client/src/assets/player/peertube-plugin.ts | |||
@@ -0,0 +1,219 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
4 | import './videojs-components/settings-menu-button' | ||
5 | import { PeerTubePluginOptions, UserWatching, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | ||
6 | import { isMobile, timeToInt } from './utils' | ||
7 | import { | ||
8 | getStoredLastSubtitle, | ||
9 | getStoredMute, | ||
10 | getStoredVolume, | ||
11 | saveLastSubtitle, | ||
12 | saveMuteInStore, | ||
13 | saveVolumeInStore | ||
14 | } from './peertube-player-local-storage' | ||
15 | |||
16 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | ||
17 | class PeerTubePlugin extends Plugin { | ||
18 | private readonly autoplay: boolean = false | ||
19 | private readonly startTime: number = 0 | ||
20 | private readonly videoViewUrl: string | ||
21 | private readonly videoDuration: number | ||
22 | private readonly CONSTANTS = { | ||
23 | USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video | ||
24 | } | ||
25 | |||
26 | private player: any | ||
27 | private videoCaptions: VideoJSCaption[] | ||
28 | private defaultSubtitle: string | ||
29 | |||
30 | private videoViewInterval: any | ||
31 | private userWatchingVideoInterval: any | ||
32 | private qualityObservationTimer: any | ||
33 | |||
34 | constructor (player: videojs.Player, options: PeerTubePluginOptions) { | ||
35 | super(player, options) | ||
36 | |||
37 | this.startTime = timeToInt(options.startTime) | ||
38 | this.videoViewUrl = options.videoViewUrl | ||
39 | this.videoDuration = options.videoDuration | ||
40 | this.videoCaptions = options.videoCaptions | ||
41 | |||
42 | if (this.autoplay === true) this.player.addClass('vjs-has-autoplay') | ||
43 | |||
44 | this.player.ready(() => { | ||
45 | const playerOptions = this.player.options_ | ||
46 | |||
47 | const volume = getStoredVolume() | ||
48 | if (volume !== undefined) this.player.volume(volume) | ||
49 | |||
50 | const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() | ||
51 | if (muted !== undefined) this.player.muted(muted) | ||
52 | |||
53 | this.defaultSubtitle = options.subtitle || getStoredLastSubtitle() | ||
54 | |||
55 | this.player.on('volumechange', () => { | ||
56 | saveVolumeInStore(this.player.volume()) | ||
57 | saveMuteInStore(this.player.muted()) | ||
58 | }) | ||
59 | |||
60 | this.player.textTracks().on('change', () => { | ||
61 | const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => { | ||
62 | return t.kind === 'captions' && t.mode === 'showing' | ||
63 | }) | ||
64 | |||
65 | if (!showing) { | ||
66 | saveLastSubtitle('off') | ||
67 | return | ||
68 | } | ||
69 | |||
70 | saveLastSubtitle(showing.language) | ||
71 | }) | ||
72 | |||
73 | this.player.on('sourcechange', () => this.initCaptions()) | ||
74 | |||
75 | this.player.duration(options.videoDuration) | ||
76 | |||
77 | this.initializePlayer() | ||
78 | this.runViewAdd() | ||
79 | |||
80 | if (options.userWatching) this.runUserWatchVideo(options.userWatching) | ||
81 | }) | ||
82 | } | ||
83 | |||
84 | dispose () { | ||
85 | clearTimeout(this.qualityObservationTimer) | ||
86 | |||
87 | clearInterval(this.videoViewInterval) | ||
88 | |||
89 | if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval) | ||
90 | } | ||
91 | |||
92 | private initializePlayer () { | ||
93 | if (isMobile()) this.player.addClass('vjs-is-mobile') | ||
94 | |||
95 | this.initSmoothProgressBar() | ||
96 | |||
97 | this.initCaptions() | ||
98 | |||
99 | this.alterInactivity() | ||
100 | } | ||
101 | |||
102 | private runViewAdd () { | ||
103 | this.clearVideoViewInterval() | ||
104 | |||
105 | // After 30 seconds (or 3/4 of the video), add a view to the video | ||
106 | let minSecondsToView = 30 | ||
107 | |||
108 | if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4 | ||
109 | |||
110 | let secondsViewed = 0 | ||
111 | this.videoViewInterval = setInterval(() => { | ||
112 | if (this.player && !this.player.paused()) { | ||
113 | secondsViewed += 1 | ||
114 | |||
115 | if (secondsViewed > minSecondsToView) { | ||
116 | this.clearVideoViewInterval() | ||
117 | |||
118 | this.addViewToVideo().catch(err => console.error(err)) | ||
119 | } | ||
120 | } | ||
121 | }, 1000) | ||
122 | } | ||
123 | |||
124 | private runUserWatchVideo (options: UserWatching) { | ||
125 | let lastCurrentTime = 0 | ||
126 | |||
127 | this.userWatchingVideoInterval = setInterval(() => { | ||
128 | const currentTime = Math.floor(this.player.currentTime()) | ||
129 | |||
130 | if (currentTime - lastCurrentTime >= 1) { | ||
131 | lastCurrentTime = currentTime | ||
132 | |||
133 | this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader) | ||
134 | .catch(err => console.error('Cannot notify user is watching.', err)) | ||
135 | } | ||
136 | }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL) | ||
137 | } | ||
138 | |||
139 | private clearVideoViewInterval () { | ||
140 | if (this.videoViewInterval !== undefined) { | ||
141 | clearInterval(this.videoViewInterval) | ||
142 | this.videoViewInterval = undefined | ||
143 | } | ||
144 | } | ||
145 | |||
146 | private addViewToVideo () { | ||
147 | if (!this.videoViewUrl) return Promise.resolve(undefined) | ||
148 | |||
149 | return fetch(this.videoViewUrl, { method: 'POST' }) | ||
150 | } | ||
151 | |||
152 | private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) { | ||
153 | const body = new URLSearchParams() | ||
154 | body.append('currentTime', currentTime.toString()) | ||
155 | |||
156 | const headers = new Headers({ 'Authorization': authorizationHeader }) | ||
157 | |||
158 | return fetch(url, { method: 'PUT', body, headers }) | ||
159 | } | ||
160 | |||
161 | private alterInactivity () { | ||
162 | let saveInactivityTimeout: number | ||
163 | |||
164 | const disableInactivity = () => { | ||
165 | saveInactivityTimeout = this.player.options_.inactivityTimeout | ||
166 | this.player.options_.inactivityTimeout = 0 | ||
167 | } | ||
168 | const enableInactivity = () => { | ||
169 | this.player.options_.inactivityTimeout = saveInactivityTimeout | ||
170 | } | ||
171 | |||
172 | const settingsDialog = this.player.children_.find((c: any) => c.name_ === 'SettingsDialog') | ||
173 | |||
174 | this.player.controlBar.on('mouseenter', () => disableInactivity()) | ||
175 | settingsDialog.on('mouseenter', () => disableInactivity()) | ||
176 | this.player.controlBar.on('mouseleave', () => enableInactivity()) | ||
177 | settingsDialog.on('mouseleave', () => enableInactivity()) | ||
178 | } | ||
179 | |||
180 | private initCaptions () { | ||
181 | for (const caption of this.videoCaptions) { | ||
182 | this.player.addRemoteTextTrack({ | ||
183 | kind: 'captions', | ||
184 | label: caption.label, | ||
185 | language: caption.language, | ||
186 | id: caption.language, | ||
187 | src: caption.src, | ||
188 | default: this.defaultSubtitle === caption.language | ||
189 | }, false) | ||
190 | } | ||
191 | |||
192 | this.player.trigger('captionsChanged') | ||
193 | } | ||
194 | |||
195 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 | ||
196 | private initSmoothProgressBar () { | ||
197 | const SeekBar = videojsUntyped.getComponent('SeekBar') | ||
198 | SeekBar.prototype.getPercent = function getPercent () { | ||
199 | // Allows for smooth scrubbing, when player can't keep up. | ||
200 | // const time = (this.player_.scrubbing()) ? | ||
201 | // this.player_.getCache().currentTime : | ||
202 | // this.player_.currentTime() | ||
203 | const time = this.player_.currentTime() | ||
204 | const percent = time / this.player_.duration() | ||
205 | return percent >= 1 ? 1 : percent | ||
206 | } | ||
207 | SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) { | ||
208 | let newTime = this.calculateDistance(event) * this.player_.duration() | ||
209 | if (newTime === this.player_.duration()) { | ||
210 | newTime = newTime - 0.1 | ||
211 | } | ||
212 | this.player_.currentTime(newTime) | ||
213 | this.update() | ||
214 | } | ||
215 | } | ||
216 | } | ||
217 | |||
218 | videojs.registerPlugin('peertube', PeerTubePlugin) | ||
219 | export { PeerTubePlugin } | ||
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts index 634c7fdc9..060ea4dce 100644 --- a/client/src/assets/player/peertube-videojs-typings.ts +++ b/client/src/assets/player/peertube-videojs-typings.ts | |||
@@ -3,11 +3,13 @@ | |||
3 | import * as videojs from 'video.js' | 3 | import * as videojs from 'video.js' |
4 | 4 | ||
5 | import { VideoFile } from '../../../../shared/models/videos/video.model' | 5 | import { VideoFile } from '../../../../shared/models/videos/video.model' |
6 | import { PeerTubePlugin } from './peertube-videojs-plugin' | 6 | import { PeerTubePlugin } from './peertube-plugin' |
7 | import { WebTorrentPlugin } from './webtorrent-plugin' | ||
7 | 8 | ||
8 | declare namespace videojs { | 9 | declare namespace videojs { |
9 | interface Player { | 10 | interface Player { |
10 | peertube (): PeerTubePlugin | 11 | peertube (): PeerTubePlugin |
12 | webtorrent (): WebTorrentPlugin | ||
11 | } | 13 | } |
12 | } | 14 | } |
13 | 15 | ||
@@ -30,26 +32,73 @@ type UserWatching = { | |||
30 | authorizationHeader: string | 32 | authorizationHeader: string |
31 | } | 33 | } |
32 | 34 | ||
33 | type PeertubePluginOptions = { | 35 | type PeerTubePluginOptions = { |
34 | videoFiles: VideoFile[] | 36 | autoplay: boolean |
35 | playerElement: HTMLVideoElement | ||
36 | videoViewUrl: string | 37 | videoViewUrl: string |
37 | videoDuration: number | 38 | videoDuration: number |
38 | startTime: number | string | 39 | startTime: number | string |
39 | autoplay: boolean, | ||
40 | videoCaptions: VideoJSCaption[] | ||
41 | 40 | ||
42 | subtitle?: string | ||
43 | userWatching?: UserWatching | 41 | userWatching?: UserWatching |
42 | subtitle?: string | ||
43 | |||
44 | videoCaptions: VideoJSCaption[] | ||
45 | } | ||
46 | |||
47 | type WebtorrentPluginOptions = { | ||
48 | playerElement: HTMLVideoElement | ||
49 | |||
50 | autoplay: boolean | ||
51 | videoDuration: number | ||
52 | |||
53 | videoFiles: VideoFile[] | ||
54 | } | ||
55 | |||
56 | type P2PMediaLoaderPluginOptions = { | ||
57 | type: string | ||
58 | src: string | ||
59 | } | ||
60 | |||
61 | type VideoJSPluginOptions = { | ||
62 | peertube: PeerTubePluginOptions | ||
63 | |||
64 | webtorrent?: WebtorrentPluginOptions | ||
65 | |||
66 | p2pMediaLoader?: P2PMediaLoaderPluginOptions | ||
44 | } | 67 | } |
45 | 68 | ||
46 | // videojs typings don't have some method we need | 69 | // videojs typings don't have some method we need |
47 | const videojsUntyped = videojs as any | 70 | const videojsUntyped = videojs as any |
48 | 71 | ||
72 | type LoadedQualityData = { | ||
73 | qualitySwitchCallback: Function, | ||
74 | qualityData: { | ||
75 | video: { | ||
76 | id: number | ||
77 | label: string | ||
78 | selected: boolean | ||
79 | }[] | ||
80 | } | ||
81 | } | ||
82 | |||
83 | type ResolutionUpdateData = { | ||
84 | auto: boolean, | ||
85 | resolutionId: number | ||
86 | } | ||
87 | |||
88 | type AutoResolutionUpdateData = { | ||
89 | possible: boolean | ||
90 | } | ||
91 | |||
49 | export { | 92 | export { |
93 | ResolutionUpdateData, | ||
94 | AutoResolutionUpdateData, | ||
50 | VideoJSComponentInterface, | 95 | VideoJSComponentInterface, |
51 | PeertubePluginOptions, | ||
52 | videojsUntyped, | 96 | videojsUntyped, |
53 | VideoJSCaption, | 97 | VideoJSCaption, |
54 | UserWatching | 98 | UserWatching, |
99 | PeerTubePluginOptions, | ||
100 | WebtorrentPluginOptions, | ||
101 | P2PMediaLoaderPluginOptions, | ||
102 | VideoJSPluginOptions, | ||
103 | LoadedQualityData | ||
55 | } | 104 | } |
diff --git a/client/src/assets/player/resolution-menu-item.ts b/client/src/assets/player/resolution-menu-item.ts deleted file mode 100644 index b54fd91ef..000000000 --- a/client/src/assets/player/resolution-menu-item.ts +++ /dev/null | |||
@@ -1,67 +0,0 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
3 | import { Player } from 'video.js' | ||
4 | |||
5 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | ||
6 | |||
7 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | ||
8 | class ResolutionMenuItem extends MenuItem { | ||
9 | |||
10 | constructor (player: Player, options: any) { | ||
11 | const currentResolutionId = player.peertube().getCurrentResolutionId() | ||
12 | options.selectable = true | ||
13 | options.selected = options.id === currentResolutionId | ||
14 | |||
15 | super(player, options) | ||
16 | |||
17 | this.label = options.label | ||
18 | this.id = options.id | ||
19 | |||
20 | player.peertube().on('videoFileUpdate', () => this.updateSelection()) | ||
21 | player.peertube().on('autoResolutionUpdate', () => this.updateSelection()) | ||
22 | } | ||
23 | |||
24 | handleClick (event: any) { | ||
25 | if (this.id === -1 && this.player_.peertube().isAutoResolutionForbidden()) return | ||
26 | |||
27 | super.handleClick(event) | ||
28 | |||
29 | // Auto resolution | ||
30 | if (this.id === -1) { | ||
31 | this.player_.peertube().enableAutoResolution() | ||
32 | return | ||
33 | } | ||
34 | |||
35 | this.player_.peertube().disableAutoResolution() | ||
36 | this.player_.peertube().updateResolution(this.id) | ||
37 | } | ||
38 | |||
39 | updateSelection () { | ||
40 | // Check if auto resolution is forbidden or not | ||
41 | if (this.id === -1) { | ||
42 | if (this.player_.peertube().isAutoResolutionForbidden()) { | ||
43 | this.addClass('disabled') | ||
44 | } else { | ||
45 | this.removeClass('disabled') | ||
46 | } | ||
47 | } | ||
48 | |||
49 | if (this.player_.peertube().isAutoResolutionOn()) { | ||
50 | this.selected(this.id === -1) | ||
51 | return | ||
52 | } | ||
53 | |||
54 | this.selected(this.player_.peertube().getCurrentResolutionId() === this.id) | ||
55 | } | ||
56 | |||
57 | getLabel () { | ||
58 | if (this.id === -1) { | ||
59 | return this.label + ' <small>' + this.player_.peertube().getCurrentResolutionLabel() + '</small>' | ||
60 | } | ||
61 | |||
62 | return this.label | ||
63 | } | ||
64 | } | ||
65 | MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) | ||
66 | |||
67 | export { ResolutionMenuItem } | ||
diff --git a/client/src/assets/player/webtorrent-info-button.ts b/client/src/assets/player/videojs-components/p2p-info-button.ts index c3c1af951..03a5d29f0 100644 --- a/client/src/assets/player/webtorrent-info-button.ts +++ b/client/src/assets/player/videojs-components/p2p-info-button.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 1 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
2 | import { bytes } from './utils' | 2 | import { bytes } from '../utils' |
3 | 3 | ||
4 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 4 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
5 | class WebtorrentInfoButton extends Button { | 5 | class P2pInfoButton extends Button { |
6 | 6 | ||
7 | createEl () { | 7 | createEl () { |
8 | const div = videojsUntyped.dom.createEl('div', { | 8 | const div = videojsUntyped.dom.createEl('div', { |
@@ -65,7 +65,7 @@ class WebtorrentInfoButton extends Button { | |||
65 | subDivHttp.appendChild(subDivHttpText) | 65 | subDivHttp.appendChild(subDivHttpText) |
66 | div.appendChild(subDivHttp) | 66 | div.appendChild(subDivHttp) |
67 | 67 | ||
68 | this.player_.peertube().on('torrentInfo', (event: any, data: any) => { | 68 | this.player_.on('p2pInfo', (event: any, data: any) => { |
69 | // We are in HTTP fallback | 69 | // We are in HTTP fallback |
70 | if (!data) { | 70 | if (!data) { |
71 | subDivHttp.className = 'vjs-peertube-displayed' | 71 | subDivHttp.className = 'vjs-peertube-displayed' |
@@ -99,4 +99,4 @@ class WebtorrentInfoButton extends Button { | |||
99 | return div | 99 | return div |
100 | } | 100 | } |
101 | } | 101 | } |
102 | Button.registerComponent('WebTorrentButton', WebtorrentInfoButton) | 102 | Button.registerComponent('P2PInfoButton', P2pInfoButton) |
diff --git a/client/src/assets/player/peertube-link-button.ts b/client/src/assets/player/videojs-components/peertube-link-button.ts index de9a49de9..fed8ea33e 100644 --- a/client/src/assets/player/peertube-link-button.ts +++ b/client/src/assets/player/videojs-components/peertube-link-button.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 1 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
2 | import { buildVideoLink } from './utils' | 2 | import { buildVideoLink } from '../utils' |
3 | // FIXME: something weird with our path definition in tsconfig and typings | 3 | // FIXME: something weird with our path definition in tsconfig and typings |
4 | // @ts-ignore | 4 | // @ts-ignore |
5 | import { Player } from 'video.js' | 5 | import { Player } from 'video.js' |
diff --git a/client/src/assets/player/peertube-load-progress-bar.ts b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts index af276d1b2..9a0e3b550 100644 --- a/client/src/assets/player/peertube-load-progress-bar.ts +++ b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 1 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
2 | // FIXME: something weird with our path definition in tsconfig and typings | 2 | // FIXME: something weird with our path definition in tsconfig and typings |
3 | // @ts-ignore | 3 | // @ts-ignore |
4 | import { Player } from 'video.js' | 4 | import { Player } from 'video.js' |
@@ -27,7 +27,7 @@ class PeerTubeLoadProgressBar extends Component { | |||
27 | } | 27 | } |
28 | 28 | ||
29 | update () { | 29 | update () { |
30 | const torrent = this.player().peertube().getTorrent() | 30 | const torrent = this.player().webtorrent().getTorrent() |
31 | if (!torrent) return | 31 | if (!torrent) return |
32 | 32 | ||
33 | this.el_.style.width = (torrent.progress * 100) + '%' | 33 | this.el_.style.width = (torrent.progress * 100) + '%' |
diff --git a/client/src/assets/player/resolution-menu-button.ts b/client/src/assets/player/videojs-components/resolution-menu-button.ts index a3c1108ca..2847de470 100644 --- a/client/src/assets/player/resolution-menu-button.ts +++ b/client/src/assets/player/videojs-components/resolution-menu-button.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | // @ts-ignore | 2 | // @ts-ignore |
3 | import { Player } from 'video.js' | 3 | import { Player } from 'video.js' |
4 | 4 | ||
5 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 5 | import { LoadedQualityData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
6 | import { ResolutionMenuItem } from './resolution-menu-item' | 6 | import { ResolutionMenuItem } from './resolution-menu-item' |
7 | 7 | ||
8 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | 8 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') |
@@ -14,16 +14,18 @@ class ResolutionMenuButton extends MenuButton { | |||
14 | super(player, options) | 14 | super(player, options) |
15 | this.player = player | 15 | this.player = player |
16 | 16 | ||
17 | player.peertube().on('videoFileUpdate', () => this.updateLabel()) | 17 | player.on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data)) |
18 | player.peertube().on('autoResolutionUpdate', () => this.updateLabel()) | 18 | |
19 | if (player.webtorrent) { | ||
20 | player.webtorrent().on('videoFileUpdate', () => setTimeout(() => this.trigger('updateLabel'), 0)) | ||
21 | } | ||
19 | } | 22 | } |
20 | 23 | ||
21 | createEl () { | 24 | createEl () { |
22 | const el = super.createEl() | 25 | const el = super.createEl() |
23 | 26 | ||
24 | this.labelEl_ = videojsUntyped.dom.createEl('div', { | 27 | this.labelEl_ = videojsUntyped.dom.createEl('div', { |
25 | className: 'vjs-resolution-value', | 28 | className: 'vjs-resolution-value' |
26 | innerHTML: this.buildLabelHTML() | ||
27 | }) | 29 | }) |
28 | 30 | ||
29 | el.appendChild(this.labelEl_) | 31 | el.appendChild(this.labelEl_) |
@@ -36,51 +38,45 @@ class ResolutionMenuButton extends MenuButton { | |||
36 | } | 38 | } |
37 | 39 | ||
38 | createMenu () { | 40 | createMenu () { |
39 | const menu = new Menu(this.player_) | 41 | return new Menu(this.player_) |
40 | for (const videoFile of this.player_.peertube().videoFiles) { | 42 | } |
41 | let label = videoFile.resolution.label | 43 | |
42 | if (videoFile.fps && videoFile.fps >= 50) { | 44 | buildCSSClass () { |
43 | label += videoFile.fps | 45 | return super.buildCSSClass() + ' vjs-resolution-button' |
44 | } | 46 | } |
45 | 47 | ||
46 | menu.addChild(new ResolutionMenuItem( | 48 | buildWrapperCSSClass () { |
49 | return 'vjs-resolution-control ' + super.buildWrapperCSSClass() | ||
50 | } | ||
51 | |||
52 | private buildQualities (data: LoadedQualityData) { | ||
53 | // The automatic resolution item will need other labels | ||
54 | const labels: { [ id: number ]: string } = {} | ||
55 | |||
56 | for (const d of data.qualityData.video) { | ||
57 | this.menu.addChild(new ResolutionMenuItem( | ||
47 | this.player_, | 58 | this.player_, |
48 | { | 59 | { |
49 | id: videoFile.resolution.id, | 60 | id: d.id, |
50 | label, | 61 | label: d.label, |
51 | src: videoFile.magnetUri | 62 | selected: d.selected, |
63 | callback: data.qualitySwitchCallback | ||
52 | }) | 64 | }) |
53 | ) | 65 | ) |
66 | |||
67 | labels[d.id] = d.label | ||
54 | } | 68 | } |
55 | 69 | ||
56 | menu.addChild(new ResolutionMenuItem( | 70 | this.menu.addChild(new ResolutionMenuItem( |
57 | this.player_, | 71 | this.player_, |
58 | { | 72 | { |
59 | id: -1, | 73 | id: -1, |
60 | label: this.player_.localize('Auto'), | 74 | label: this.player_.localize('Auto'), |
61 | src: null | 75 | labels, |
76 | callback: data.qualitySwitchCallback, | ||
77 | selected: true // By default, in auto mode | ||
62 | } | 78 | } |
63 | )) | 79 | )) |
64 | |||
65 | return menu | ||
66 | } | ||
67 | |||
68 | updateLabel () { | ||
69 | if (!this.labelEl_) return | ||
70 | |||
71 | this.labelEl_.innerHTML = this.buildLabelHTML() | ||
72 | } | ||
73 | |||
74 | buildCSSClass () { | ||
75 | return super.buildCSSClass() + ' vjs-resolution-button' | ||
76 | } | ||
77 | |||
78 | buildWrapperCSSClass () { | ||
79 | return 'vjs-resolution-control ' + super.buildWrapperCSSClass() | ||
80 | } | ||
81 | |||
82 | private buildLabelHTML () { | ||
83 | return this.player_.peertube().getCurrentResolutionLabel() | ||
84 | } | 80 | } |
85 | } | 81 | } |
86 | ResolutionMenuButton.prototype.controlText_ = 'Quality' | 82 | ResolutionMenuButton.prototype.controlText_ = 'Quality' |
diff --git a/client/src/assets/player/videojs-components/resolution-menu-item.ts b/client/src/assets/player/videojs-components/resolution-menu-item.ts new file mode 100644 index 000000000..cc1c79739 --- /dev/null +++ b/client/src/assets/player/videojs-components/resolution-menu-item.ts | |||
@@ -0,0 +1,87 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
3 | import { Player } from 'video.js' | ||
4 | |||
5 | import { AutoResolutionUpdateData, ResolutionUpdateData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | ||
6 | |||
7 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | ||
8 | class ResolutionMenuItem extends MenuItem { | ||
9 | private readonly id: number | ||
10 | private readonly label: string | ||
11 | // Only used for the automatic item | ||
12 | private readonly labels: { [id: number]: string } | ||
13 | private readonly callback: Function | ||
14 | |||
15 | private autoResolutionPossible: boolean | ||
16 | private currentResolutionLabel: string | ||
17 | |||
18 | constructor (player: Player, options: any) { | ||
19 | options.selectable = true | ||
20 | |||
21 | super(player, options) | ||
22 | |||
23 | this.autoResolutionPossible = true | ||
24 | this.currentResolutionLabel = '' | ||
25 | |||
26 | this.label = options.label | ||
27 | this.labels = options.labels | ||
28 | this.id = options.id | ||
29 | this.callback = options.callback | ||
30 | |||
31 | if (player.webtorrent) { | ||
32 | player.webtorrent().on('videoFileUpdate', (_: any, data: ResolutionUpdateData) => this.updateSelection(data)) | ||
33 | |||
34 | // We only want to disable the "Auto" item | ||
35 | if (this.id === -1) { | ||
36 | player.webtorrent().on('autoResolutionUpdate', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data)) | ||
37 | } | ||
38 | } | ||
39 | |||
40 | // TODO: update on HLS change | ||
41 | } | ||
42 | |||
43 | handleClick (event: any) { | ||
44 | // Auto button disabled? | ||
45 | if (this.autoResolutionPossible === false && this.id === -1) return | ||
46 | |||
47 | super.handleClick(event) | ||
48 | |||
49 | this.callback(this.id) | ||
50 | } | ||
51 | |||
52 | updateSelection (data: ResolutionUpdateData) { | ||
53 | if (this.id === -1) { | ||
54 | this.currentResolutionLabel = this.labels[data.resolutionId] | ||
55 | } | ||
56 | |||
57 | // Automatic resolution only | ||
58 | if (data.auto === true) { | ||
59 | this.selected(this.id === -1) | ||
60 | return | ||
61 | } | ||
62 | |||
63 | this.selected(this.id === data.resolutionId) | ||
64 | } | ||
65 | |||
66 | updateAutoResolution (data: AutoResolutionUpdateData) { | ||
67 | // Check if the auto resolution is enabled or not | ||
68 | if (data.possible === false) { | ||
69 | this.addClass('disabled') | ||
70 | } else { | ||
71 | this.removeClass('disabled') | ||
72 | } | ||
73 | |||
74 | this.autoResolutionPossible = data.possible | ||
75 | } | ||
76 | |||
77 | getLabel () { | ||
78 | if (this.id === -1) { | ||
79 | return this.label + ' <small>' + this.currentResolutionLabel + '</small>' | ||
80 | } | ||
81 | |||
82 | return this.label | ||
83 | } | ||
84 | } | ||
85 | MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) | ||
86 | |||
87 | export { ResolutionMenuItem } | ||
diff --git a/client/src/assets/player/settings-menu-button.ts b/client/src/assets/player/videojs-components/settings-menu-button.ts index a7aefdcc3..14cb8ba43 100644 --- a/client/src/assets/player/settings-menu-button.ts +++ b/client/src/assets/player/videojs-components/settings-menu-button.ts | |||
@@ -6,8 +6,8 @@ | |||
6 | import * as videojs from 'video.js' | 6 | import * as videojs from 'video.js' |
7 | 7 | ||
8 | import { SettingsMenuItem } from './settings-menu-item' | 8 | import { SettingsMenuItem } from './settings-menu-item' |
9 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 9 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
10 | import { toTitleCase } from './utils' | 10 | import { toTitleCase } from '../utils' |
11 | 11 | ||
12 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 12 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
13 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | 13 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') |
diff --git a/client/src/assets/player/settings-menu-item.ts b/client/src/assets/player/videojs-components/settings-menu-item.ts index 2a3460ae5..b9a430290 100644 --- a/client/src/assets/player/settings-menu-item.ts +++ b/client/src/assets/player/videojs-components/settings-menu-item.ts | |||
@@ -5,8 +5,8 @@ | |||
5 | // @ts-ignore | 5 | // @ts-ignore |
6 | import * as videojs from 'video.js' | 6 | import * as videojs from 'video.js' |
7 | 7 | ||
8 | import { toTitleCase } from './utils' | 8 | import { toTitleCase } from '../utils' |
9 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 9 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
10 | 10 | ||
11 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | 11 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') |
12 | const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') | 12 | const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') |
@@ -220,12 +220,9 @@ class SettingsMenuItem extends MenuItem { | |||
220 | } | 220 | } |
221 | 221 | ||
222 | build () { | 222 | build () { |
223 | const saveUpdateLabel = this.subMenu.updateLabel | 223 | this.subMenu.on('updateLabel', () => { |
224 | this.subMenu.updateLabel = () => { | ||
225 | this.update() | 224 | this.update() |
226 | 225 | }) | |
227 | saveUpdateLabel.call(this.subMenu) | ||
228 | } | ||
229 | 226 | ||
230 | this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_) | 227 | this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_) |
231 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) | 228 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) |
diff --git a/client/src/assets/player/theater-button.ts b/client/src/assets/player/videojs-components/theater-button.ts index 4f8fede3d..1e11a9546 100644 --- a/client/src/assets/player/theater-button.ts +++ b/client/src/assets/player/videojs-components/theater-button.ts | |||
@@ -2,8 +2,8 @@ | |||
2 | // @ts-ignore | 2 | // @ts-ignore |
3 | import * as videojs from 'video.js' | 3 | import * as videojs from 'video.js' |
4 | 4 | ||
5 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 5 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
6 | import { saveTheaterInStore, getStoredTheater } from './peertube-player-local-storage' | 6 | import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage' |
7 | 7 | ||
8 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 8 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
9 | class TheaterButton extends Button { | 9 | class TheaterButton extends Button { |
diff --git a/client/src/assets/player/peertube-videojs-plugin.ts b/client/src/assets/player/webtorrent-plugin.ts index e9fb90c61..c3d990aed 100644 --- a/client/src/assets/player/peertube-videojs-plugin.ts +++ b/client/src/assets/player/webtorrent-plugin.ts | |||
@@ -4,21 +4,16 @@ import * as videojs from 'video.js' | |||
4 | 4 | ||
5 | import * as WebTorrent from 'webtorrent' | 5 | import * as WebTorrent from 'webtorrent' |
6 | import { VideoFile } from '../../../../shared/models/videos/video.model' | 6 | import { VideoFile } from '../../../../shared/models/videos/video.model' |
7 | import { renderVideo } from './video-renderer' | 7 | import { renderVideo } from './webtorrent/video-renderer' |
8 | import './settings-menu-button' | 8 | import { LoadedQualityData, VideoJSComponentInterface, WebtorrentPluginOptions } from './peertube-videojs-typings' |
9 | import { PeertubePluginOptions, UserWatching, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 9 | import { videoFileMaxByResolution, videoFileMinByResolution } from './utils' |
10 | import { isMobile, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from './utils' | 10 | import { PeertubeChunkStore } from './webtorrent/peertube-chunk-store' |
11 | import { PeertubeChunkStore } from './peertube-chunk-store' | ||
12 | import { | 11 | import { |
13 | getAverageBandwidthInStore, | 12 | getAverageBandwidthInStore, |
14 | getStoredLastSubtitle, | ||
15 | getStoredMute, | 13 | getStoredMute, |
16 | getStoredVolume, | 14 | getStoredVolume, |
17 | getStoredWebTorrentEnabled, | 15 | getStoredWebTorrentEnabled, |
18 | saveAverageBandwidth, | 16 | saveAverageBandwidth |
19 | saveLastSubtitle, | ||
20 | saveMuteInStore, | ||
21 | saveVolumeInStore | ||
22 | } from './peertube-player-local-storage' | 17 | } from './peertube-player-local-storage' |
23 | 18 | ||
24 | const CacheChunkStore = require('cache-chunk-store') | 19 | const CacheChunkStore = require('cache-chunk-store') |
@@ -30,14 +25,13 @@ type PlayOptions = { | |||
30 | } | 25 | } |
31 | 26 | ||
32 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | 27 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') |
33 | class PeerTubePlugin extends Plugin { | 28 | class WebTorrentPlugin extends Plugin { |
34 | private readonly playerElement: HTMLVideoElement | 29 | private readonly playerElement: HTMLVideoElement |
35 | 30 | ||
36 | private readonly autoplay: boolean = false | 31 | private readonly autoplay: boolean = false |
37 | private readonly startTime: number = 0 | 32 | private readonly startTime: number = 0 |
38 | private readonly savePlayerSrcFunction: Function | 33 | private readonly savePlayerSrcFunction: Function |
39 | private readonly videoFiles: VideoFile[] | 34 | private readonly videoFiles: VideoFile[] |
40 | private readonly videoViewUrl: string | ||
41 | private readonly videoDuration: number | 35 | private readonly videoDuration: number |
42 | private readonly CONSTANTS = { | 36 | private readonly CONSTANTS = { |
43 | INFO_SCHEDULER: 1000, // Don't change this | 37 | INFO_SCHEDULER: 1000, // Don't change this |
@@ -45,8 +39,7 @@ class PeerTubePlugin extends Plugin { | |||
45 | AUTO_QUALITY_THRESHOLD_PERCENT: 30, // Bandwidth should be 30% more important than a resolution bitrate to change to it | 39 | AUTO_QUALITY_THRESHOLD_PERCENT: 30, // Bandwidth should be 30% more important than a resolution bitrate to change to it |
46 | AUTO_QUALITY_OBSERVATION_TIME: 10000, // Wait 10 seconds after having change the resolution before another check | 40 | AUTO_QUALITY_OBSERVATION_TIME: 10000, // Wait 10 seconds after having change the resolution before another check |
47 | AUTO_QUALITY_HIGHER_RESOLUTION_DELAY: 5000, // Buffering higher resolution during 5 seconds | 41 | AUTO_QUALITY_HIGHER_RESOLUTION_DELAY: 5000, // Buffering higher resolution during 5 seconds |
48 | BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5, // Last 5 seconds to build average bandwidth | 42 | BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5 // Last 5 seconds to build average bandwidth |
49 | USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video | ||
50 | } | 43 | } |
51 | 44 | ||
52 | private readonly webtorrent = new WebTorrent({ | 45 | private readonly webtorrent = new WebTorrent({ |
@@ -68,46 +61,37 @@ class PeerTubePlugin extends Plugin { | |||
68 | private player: any | 61 | private player: any |
69 | private currentVideoFile: VideoFile | 62 | private currentVideoFile: VideoFile |
70 | private torrent: WebTorrent.Torrent | 63 | private torrent: WebTorrent.Torrent |
71 | private videoCaptions: VideoJSCaption[] | ||
72 | private defaultSubtitle: string | ||
73 | 64 | ||
74 | private renderer: any | 65 | private renderer: any |
75 | private fakeRenderer: any | 66 | private fakeRenderer: any |
76 | private destroyingFakeRenderer = false | 67 | private destroyingFakeRenderer = false |
77 | 68 | ||
78 | private autoResolution = true | 69 | private autoResolution = true |
79 | private forbidAutoResolution = false | 70 | private autoResolutionPossible = true |
80 | private isAutoResolutionObservation = false | 71 | private isAutoResolutionObservation = false |
81 | private playerRefusedP2P = false | 72 | private playerRefusedP2P = false |
82 | 73 | ||
83 | private videoViewInterval: any | ||
84 | private torrentInfoInterval: any | 74 | private torrentInfoInterval: any |
85 | private autoQualityInterval: any | 75 | private autoQualityInterval: any |
86 | private userWatchingVideoInterval: any | ||
87 | private addTorrentDelay: any | 76 | private addTorrentDelay: any |
88 | private qualityObservationTimer: any | 77 | private qualityObservationTimer: any |
89 | private runAutoQualitySchedulerTimer: any | 78 | private runAutoQualitySchedulerTimer: any |
90 | 79 | ||
91 | private downloadSpeeds: number[] = [] | 80 | private downloadSpeeds: number[] = [] |
92 | 81 | ||
93 | constructor (player: videojs.Player, options: PeertubePluginOptions) { | 82 | constructor (player: videojs.Player, options: WebtorrentPluginOptions) { |
94 | super(player, options) | 83 | super(player, options) |
95 | 84 | ||
96 | // Disable auto play on iOS | 85 | // Disable auto play on iOS |
97 | this.autoplay = options.autoplay && this.isIOS() === false | 86 | this.autoplay = options.autoplay && this.isIOS() === false |
98 | this.playerRefusedP2P = !getStoredWebTorrentEnabled() | 87 | this.playerRefusedP2P = !getStoredWebTorrentEnabled() |
99 | 88 | ||
100 | this.startTime = timeToInt(options.startTime) | ||
101 | this.videoFiles = options.videoFiles | 89 | this.videoFiles = options.videoFiles |
102 | this.videoViewUrl = options.videoViewUrl | ||
103 | this.videoDuration = options.videoDuration | 90 | this.videoDuration = options.videoDuration |
104 | this.videoCaptions = options.videoCaptions | ||
105 | 91 | ||
106 | this.savePlayerSrcFunction = this.player.src | 92 | this.savePlayerSrcFunction = this.player.src |
107 | this.playerElement = options.playerElement | 93 | this.playerElement = options.playerElement |
108 | 94 | ||
109 | if (this.autoplay === true) this.player.addClass('vjs-has-autoplay') | ||
110 | |||
111 | this.player.ready(() => { | 95 | this.player.ready(() => { |
112 | const playerOptions = this.player.options_ | 96 | const playerOptions = this.player.options_ |
113 | 97 | ||
@@ -117,33 +101,10 @@ class PeerTubePlugin extends Plugin { | |||
117 | const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() | 101 | const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() |
118 | if (muted !== undefined) this.player.muted(muted) | 102 | if (muted !== undefined) this.player.muted(muted) |
119 | 103 | ||
120 | this.defaultSubtitle = options.subtitle || getStoredLastSubtitle() | ||
121 | |||
122 | this.player.on('volumechange', () => { | ||
123 | saveVolumeInStore(this.player.volume()) | ||
124 | saveMuteInStore(this.player.muted()) | ||
125 | }) | ||
126 | |||
127 | this.player.textTracks().on('change', () => { | ||
128 | const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => { | ||
129 | return t.kind === 'captions' && t.mode === 'showing' | ||
130 | }) | ||
131 | |||
132 | if (!showing) { | ||
133 | saveLastSubtitle('off') | ||
134 | return | ||
135 | } | ||
136 | |||
137 | saveLastSubtitle(showing.language) | ||
138 | }) | ||
139 | |||
140 | this.player.duration(options.videoDuration) | 104 | this.player.duration(options.videoDuration) |
141 | 105 | ||
142 | this.initializePlayer() | 106 | this.initializePlayer() |
143 | this.runTorrentInfoScheduler() | 107 | this.runTorrentInfoScheduler() |
144 | this.runViewAdd() | ||
145 | |||
146 | if (options.userWatching) this.runUserWatchVideo(options.userWatching) | ||
147 | 108 | ||
148 | this.player.one('play', () => { | 109 | this.player.one('play', () => { |
149 | // Don't run immediately scheduler, wait some seconds the TCP connections are made | 110 | // Don't run immediately scheduler, wait some seconds the TCP connections are made |
@@ -157,12 +118,9 @@ class PeerTubePlugin extends Plugin { | |||
157 | clearTimeout(this.qualityObservationTimer) | 118 | clearTimeout(this.qualityObservationTimer) |
158 | clearTimeout(this.runAutoQualitySchedulerTimer) | 119 | clearTimeout(this.runAutoQualitySchedulerTimer) |
159 | 120 | ||
160 | clearInterval(this.videoViewInterval) | ||
161 | clearInterval(this.torrentInfoInterval) | 121 | clearInterval(this.torrentInfoInterval) |
162 | clearInterval(this.autoQualityInterval) | 122 | clearInterval(this.autoQualityInterval) |
163 | 123 | ||
164 | if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval) | ||
165 | |||
166 | // Don't need to destroy renderer, video player will be destroyed | 124 | // Don't need to destroy renderer, video player will be destroyed |
167 | this.flushVideoFile(this.currentVideoFile, false) | 125 | this.flushVideoFile(this.currentVideoFile, false) |
168 | 126 | ||
@@ -173,13 +131,6 @@ class PeerTubePlugin extends Plugin { | |||
173 | return this.currentVideoFile ? this.currentVideoFile.resolution.id : -1 | 131 | return this.currentVideoFile ? this.currentVideoFile.resolution.id : -1 |
174 | } | 132 | } |
175 | 133 | ||
176 | getCurrentResolutionLabel () { | ||
177 | if (!this.currentVideoFile) return '' | ||
178 | |||
179 | const fps = this.currentVideoFile.fps >= 50 ? this.currentVideoFile.fps : '' | ||
180 | return this.currentVideoFile.resolution.label + fps | ||
181 | } | ||
182 | |||
183 | updateVideoFile ( | 134 | updateVideoFile ( |
184 | videoFile?: VideoFile, | 135 | videoFile?: VideoFile, |
185 | options: { | 136 | options: { |
@@ -228,7 +179,8 @@ class PeerTubePlugin extends Plugin { | |||
228 | return done() | 179 | return done() |
229 | }) | 180 | }) |
230 | 181 | ||
231 | this.trigger('videoFileUpdate') | 182 | this.changeQuality() |
183 | this.trigger('videoFileUpdate', { auto: this.autoResolution, resolutionId: this.currentVideoFile.resolution.id }) | ||
232 | } | 184 | } |
233 | 185 | ||
234 | updateResolution (resolutionId: number, delay = 0) { | 186 | updateResolution (resolutionId: number, delay = 0) { |
@@ -262,28 +214,17 @@ class PeerTubePlugin extends Plugin { | |||
262 | } | 214 | } |
263 | } | 215 | } |
264 | 216 | ||
265 | isAutoResolutionOn () { | ||
266 | return this.autoResolution | ||
267 | } | ||
268 | |||
269 | enableAutoResolution () { | 217 | enableAutoResolution () { |
270 | this.autoResolution = true | 218 | this.autoResolution = true |
271 | this.trigger('autoResolutionUpdate') | 219 | this.trigger('videoFileUpdate', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() }) |
272 | } | 220 | } |
273 | 221 | ||
274 | disableAutoResolution (forbid = false) { | 222 | disableAutoResolution (forbid = false) { |
275 | if (forbid === true) this.forbidAutoResolution = true | 223 | if (forbid === true) this.autoResolutionPossible = false |
276 | 224 | ||
277 | this.autoResolution = false | 225 | this.autoResolution = false |
278 | this.trigger('autoResolutionUpdate') | 226 | this.trigger('autoResolutionUpdate', { possible: this.autoResolutionPossible }) |
279 | } | 227 | this.trigger('videoFileUpdate', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() }) |
280 | |||
281 | isAutoResolutionForbidden () { | ||
282 | return this.forbidAutoResolution === true | ||
283 | } | ||
284 | |||
285 | getCurrentVideoFile () { | ||
286 | return this.currentVideoFile | ||
287 | } | 228 | } |
288 | 229 | ||
289 | getTorrent () { | 230 | getTorrent () { |
@@ -462,13 +403,7 @@ class PeerTubePlugin extends Plugin { | |||
462 | } | 403 | } |
463 | 404 | ||
464 | private initializePlayer () { | 405 | private initializePlayer () { |
465 | if (isMobile()) this.player.addClass('vjs-is-mobile') | 406 | this.buildQualities() |
466 | |||
467 | this.initSmoothProgressBar() | ||
468 | |||
469 | this.initCaptions() | ||
470 | |||
471 | this.alterInactivity() | ||
472 | 407 | ||
473 | if (this.autoplay === true) { | 408 | if (this.autoplay === true) { |
474 | this.player.posterImage.hide() | 409 | this.player.posterImage.hide() |
@@ -491,7 +426,7 @@ class PeerTubePlugin extends Plugin { | |||
491 | 426 | ||
492 | // Not initialized or in HTTP fallback | 427 | // Not initialized or in HTTP fallback |
493 | if (this.torrent === undefined || this.torrent === null) return | 428 | if (this.torrent === undefined || this.torrent === null) return |
494 | if (this.isAutoResolutionOn() === false) return | 429 | if (this.autoResolution === false) return |
495 | if (this.isAutoResolutionObservation === true) return | 430 | if (this.isAutoResolutionObservation === true) return |
496 | 431 | ||
497 | const file = this.getAppropriateFile() | 432 | const file = this.getAppropriateFile() |
@@ -531,12 +466,12 @@ class PeerTubePlugin extends Plugin { | |||
531 | if (this.torrent === undefined) return | 466 | if (this.torrent === undefined) return |
532 | 467 | ||
533 | // Http fallback | 468 | // Http fallback |
534 | if (this.torrent === null) return this.trigger('torrentInfo', false) | 469 | if (this.torrent === null) return this.player.trigger('p2pInfo', false) |
535 | 470 | ||
536 | // this.webtorrent.downloadSpeed because we need to take into account the potential old torrent too | 471 | // this.webtorrent.downloadSpeed because we need to take into account the potential old torrent too |
537 | if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed) | 472 | if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed) |
538 | 473 | ||
539 | return this.trigger('torrentInfo', { | 474 | return this.player.trigger('p2pInfo', { |
540 | downloadSpeed: this.torrent.downloadSpeed, | 475 | downloadSpeed: this.torrent.downloadSpeed, |
541 | numPeers: this.torrent.numPeers, | 476 | numPeers: this.torrent.numPeers, |
542 | uploadSpeed: this.torrent.uploadSpeed, | 477 | uploadSpeed: this.torrent.uploadSpeed, |
@@ -546,65 +481,6 @@ class PeerTubePlugin extends Plugin { | |||
546 | }, this.CONSTANTS.INFO_SCHEDULER) | 481 | }, this.CONSTANTS.INFO_SCHEDULER) |
547 | } | 482 | } |
548 | 483 | ||
549 | private runViewAdd () { | ||
550 | this.clearVideoViewInterval() | ||
551 | |||
552 | // After 30 seconds (or 3/4 of the video), add a view to the video | ||
553 | let minSecondsToView = 30 | ||
554 | |||
555 | if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4 | ||
556 | |||
557 | let secondsViewed = 0 | ||
558 | this.videoViewInterval = setInterval(() => { | ||
559 | if (this.player && !this.player.paused()) { | ||
560 | secondsViewed += 1 | ||
561 | |||
562 | if (secondsViewed > minSecondsToView) { | ||
563 | this.clearVideoViewInterval() | ||
564 | |||
565 | this.addViewToVideo().catch(err => console.error(err)) | ||
566 | } | ||
567 | } | ||
568 | }, 1000) | ||
569 | } | ||
570 | |||
571 | private runUserWatchVideo (options: UserWatching) { | ||
572 | let lastCurrentTime = 0 | ||
573 | |||
574 | this.userWatchingVideoInterval = setInterval(() => { | ||
575 | const currentTime = Math.floor(this.player.currentTime()) | ||
576 | |||
577 | if (currentTime - lastCurrentTime >= 1) { | ||
578 | lastCurrentTime = currentTime | ||
579 | |||
580 | this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader) | ||
581 | .catch(err => console.error('Cannot notify user is watching.', err)) | ||
582 | } | ||
583 | }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL) | ||
584 | } | ||
585 | |||
586 | private clearVideoViewInterval () { | ||
587 | if (this.videoViewInterval !== undefined) { | ||
588 | clearInterval(this.videoViewInterval) | ||
589 | this.videoViewInterval = undefined | ||
590 | } | ||
591 | } | ||
592 | |||
593 | private addViewToVideo () { | ||
594 | if (!this.videoViewUrl) return Promise.resolve(undefined) | ||
595 | |||
596 | return fetch(this.videoViewUrl, { method: 'POST' }) | ||
597 | } | ||
598 | |||
599 | private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) { | ||
600 | const body = new URLSearchParams() | ||
601 | body.append('currentTime', currentTime.toString()) | ||
602 | |||
603 | const headers = new Headers({ 'Authorization': authorizationHeader }) | ||
604 | |||
605 | return fetch(url, { method: 'PUT', body, headers }) | ||
606 | } | ||
607 | |||
608 | private fallbackToHttp (options: PlayOptions, done?: Function) { | 484 | private fallbackToHttp (options: PlayOptions, done?: Function) { |
609 | const paused = this.player.paused() | 485 | const paused = this.player.paused() |
610 | 486 | ||
@@ -620,8 +496,10 @@ class PeerTubePlugin extends Plugin { | |||
620 | this.player.src = this.savePlayerSrcFunction | 496 | this.player.src = this.savePlayerSrcFunction |
621 | this.player.src(httpUrl) | 497 | this.player.src(httpUrl) |
622 | 498 | ||
499 | this.changeQuality() | ||
500 | |||
623 | // We changed the source, so reinit captions | 501 | // We changed the source, so reinit captions |
624 | this.initCaptions() | 502 | this.player.trigger('sourcechange') |
625 | 503 | ||
626 | return this.tryToPlay(err => { | 504 | return this.tryToPlay(err => { |
627 | if (err && done) return done(err) | 505 | if (err && done) return done(err) |
@@ -649,25 +527,6 @@ class PeerTubePlugin extends Plugin { | |||
649 | return !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform) | 527 | return !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform) |
650 | } | 528 | } |
651 | 529 | ||
652 | private alterInactivity () { | ||
653 | let saveInactivityTimeout: number | ||
654 | |||
655 | const disableInactivity = () => { | ||
656 | saveInactivityTimeout = this.player.options_.inactivityTimeout | ||
657 | this.player.options_.inactivityTimeout = 0 | ||
658 | } | ||
659 | const enableInactivity = () => { | ||
660 | this.player.options_.inactivityTimeout = saveInactivityTimeout | ||
661 | } | ||
662 | |||
663 | const settingsDialog = this.player.children_.find((c: any) => c.name_ === 'SettingsDialog') | ||
664 | |||
665 | this.player.controlBar.on('mouseenter', () => disableInactivity()) | ||
666 | settingsDialog.on('mouseenter', () => disableInactivity()) | ||
667 | this.player.controlBar.on('mouseleave', () => enableInactivity()) | ||
668 | settingsDialog.on('mouseleave', () => enableInactivity()) | ||
669 | } | ||
670 | |||
671 | private pickAverageVideoFile () { | 530 | private pickAverageVideoFile () { |
672 | if (this.videoFiles.length === 1) return this.videoFiles[0] | 531 | if (this.videoFiles.length === 1) return this.videoFiles[0] |
673 | 532 | ||
@@ -712,43 +571,70 @@ class PeerTubePlugin extends Plugin { | |||
712 | } | 571 | } |
713 | } | 572 | } |
714 | 573 | ||
715 | private initCaptions () { | 574 | private buildQualities () { |
716 | for (const caption of this.videoCaptions) { | 575 | const qualityLevelsPayload = [] |
717 | this.player.addRemoteTextTrack({ | 576 | |
718 | kind: 'captions', | 577 | for (const file of this.videoFiles) { |
719 | label: caption.label, | 578 | const representation = { |
720 | language: caption.language, | 579 | id: file.resolution.id, |
721 | id: caption.language, | 580 | label: this.buildQualityLabel(file), |
722 | src: caption.src, | 581 | height: file.resolution.id, |
723 | default: this.defaultSubtitle === caption.language | 582 | _enabled: true |
724 | }, false) | 583 | } |
584 | |||
585 | this.player.qualityLevels().addQualityLevel(representation) | ||
586 | |||
587 | qualityLevelsPayload.push({ | ||
588 | id: representation.id, | ||
589 | label: representation.label, | ||
590 | selected: false | ||
591 | }) | ||
725 | } | 592 | } |
726 | 593 | ||
727 | this.player.trigger('captionsChanged') | 594 | const payload: LoadedQualityData = { |
595 | qualitySwitchCallback: (d: any) => this.qualitySwitchCallback(d), | ||
596 | qualityData: { | ||
597 | video: qualityLevelsPayload | ||
598 | } | ||
599 | } | ||
600 | this.player.trigger('loadedqualitydata', payload) | ||
728 | } | 601 | } |
729 | 602 | ||
730 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 | 603 | private buildQualityLabel (file: VideoFile) { |
731 | private initSmoothProgressBar () { | 604 | let label = file.resolution.label |
732 | const SeekBar = videojsUntyped.getComponent('SeekBar') | 605 | |
733 | SeekBar.prototype.getPercent = function getPercent () { | 606 | if (file.fps && file.fps >= 50) { |
734 | // Allows for smooth scrubbing, when player can't keep up. | 607 | label += file.fps |
735 | // const time = (this.player_.scrubbing()) ? | ||
736 | // this.player_.getCache().currentTime : | ||
737 | // this.player_.currentTime() | ||
738 | const time = this.player_.currentTime() | ||
739 | const percent = time / this.player_.duration() | ||
740 | return percent >= 1 ? 1 : percent | ||
741 | } | 608 | } |
742 | SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) { | 609 | |
743 | let newTime = this.calculateDistance(event) * this.player_.duration() | 610 | return label |
744 | if (newTime === this.player_.duration()) { | 611 | } |
745 | newTime = newTime - 0.1 | 612 | |
746 | } | 613 | private qualitySwitchCallback (id: number) { |
747 | this.player_.currentTime(newTime) | 614 | if (id === -1) { |
748 | this.update() | 615 | if (this.autoResolutionPossible === true) this.enableAutoResolution() |
616 | return | ||
617 | } | ||
618 | |||
619 | this.disableAutoResolution() | ||
620 | this.updateResolution(id) | ||
621 | } | ||
622 | |||
623 | private changeQuality () { | ||
624 | const resolutionId = this.currentVideoFile.resolution.id | ||
625 | const qualityLevels = this.player.qualityLevels() | ||
626 | |||
627 | if (resolutionId === -1) { | ||
628 | qualityLevels.selectedIndex = -1 | ||
629 | return | ||
630 | } | ||
631 | |||
632 | for (let i = 0; i < qualityLevels; i++) { | ||
633 | const q = this.player.qualityLevels[i] | ||
634 | if (q.height === resolutionId) qualityLevels.selectedIndex = i | ||
749 | } | 635 | } |
750 | } | 636 | } |
751 | } | 637 | } |
752 | 638 | ||
753 | videojs.registerPlugin('peertube', PeerTubePlugin) | 639 | videojs.registerPlugin('webtorrent', WebTorrentPlugin) |
754 | export { PeerTubePlugin } | 640 | export { WebTorrentPlugin } |
diff --git a/client/src/assets/player/peertube-chunk-store.ts b/client/src/assets/player/webtorrent/peertube-chunk-store.ts index 54cc0ea64..54cc0ea64 100644 --- a/client/src/assets/player/peertube-chunk-store.ts +++ b/client/src/assets/player/webtorrent/peertube-chunk-store.ts | |||
diff --git a/client/src/assets/player/video-renderer.ts b/client/src/assets/player/webtorrent/video-renderer.ts index a3415937b..a3415937b 100644 --- a/client/src/assets/player/video-renderer.ts +++ b/client/src/assets/player/webtorrent/video-renderer.ts | |||