aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-01-23 15:36:45 +0100
committerChocobozzz <chocobozzz@cpy.re>2019-02-11 09:13:02 +0100
commit2adfc7ea9a1f858db874df9fe322e7ae833db77c (patch)
treee27c6ebe01b7c96ea0e053839a38fc1f824d1284 /client/src
parent7eeb6a0ba4028d0e20847b846332dd0b7747c7f8 (diff)
downloadPeerTube-2adfc7ea9a1f858db874df9fe322e7ae833db77c.tar.gz
PeerTube-2adfc7ea9a1f858db874df9fe322e7ae833db77c.tar.zst
PeerTube-2adfc7ea9a1f858db874df9fe322e7ae833db77c.zip
Refractor videojs player
Add fake p2p-media-loader plugin
Diffstat (limited to 'client/src')
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts77
-rw-r--r--client/src/assets/player/p2p-media-loader-plugin.ts33
-rw-r--r--client/src/assets/player/peertube-player-manager.ts388
-rw-r--r--client/src/assets/player/peertube-player.ts300
-rw-r--r--client/src/assets/player/peertube-plugin.ts219
-rw-r--r--client/src/assets/player/peertube-videojs-typings.ts67
-rw-r--r--client/src/assets/player/resolution-menu-item.ts67
-rw-r--r--client/src/assets/player/videojs-components/p2p-info-button.ts (renamed from client/src/assets/player/webtorrent-info-button.ts)10
-rw-r--r--client/src/assets/player/videojs-components/peertube-link-button.ts (renamed from client/src/assets/player/peertube-link-button.ts)4
-rw-r--r--client/src/assets/player/videojs-components/peertube-load-progress-bar.ts (renamed from client/src/assets/player/peertube-load-progress-bar.ts)4
-rw-r--r--client/src/assets/player/videojs-components/resolution-menu-button.ts (renamed from client/src/assets/player/resolution-menu-button.ts)70
-rw-r--r--client/src/assets/player/videojs-components/resolution-menu-item.ts87
-rw-r--r--client/src/assets/player/videojs-components/settings-menu-button.ts (renamed from client/src/assets/player/settings-menu-button.ts)4
-rw-r--r--client/src/assets/player/videojs-components/settings-menu-item.ts (renamed from client/src/assets/player/settings-menu-item.ts)11
-rw-r--r--client/src/assets/player/videojs-components/theater-button.ts (renamed from client/src/assets/player/theater-button.ts)4
-rw-r--r--client/src/assets/player/webtorrent-plugin.ts (renamed from client/src/assets/player/peertube-videojs-plugin.ts)274
-rw-r--r--client/src/assets/player/webtorrent/peertube-chunk-store.ts (renamed from client/src/assets/player/peertube-chunk-store.ts)0
-rw-r--r--client/src/assets/player/webtorrent/video-renderer.ts (renamed from client/src/assets/player/video-renderer.ts)0
-rw-r--r--client/src/standalone/videos/embed.ts108
-rw-r--r--client/src/tsconfig.app.json2
20 files changed, 1012 insertions, 717 deletions
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index ee504bc58..6e38af195 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -7,14 +7,9 @@ import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-supp
7import { MetaService } from '@ngx-meta/core' 7import { MetaService } from '@ngx-meta/core'
8import { Notifier, ServerService } from '@app/core' 8import { Notifier, ServerService } from '@app/core'
9import { forkJoin, Subscription } from 'rxjs' 9import { forkJoin, Subscription } from 'rxjs'
10// FIXME: something weird with our path definition in tsconfig and typings
11// @ts-ignore
12import videojs from 'video.js'
13import 'videojs-hotkeys'
14import { Hotkey, HotkeysService } from 'angular2-hotkeys' 10import { Hotkey, HotkeysService } from 'angular2-hotkeys'
15import * as WebTorrent from 'webtorrent' 11import * as WebTorrent from 'webtorrent'
16import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared' 12import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared'
17import '../../../assets/player/peertube-videojs-plugin'
18import { AuthService, ConfirmService } from '../../core' 13import { AuthService, ConfirmService } from '../../core'
19import { RestExtractor, VideoBlacklistService } from '../../shared' 14import { RestExtractor, VideoBlacklistService } from '../../shared'
20import { VideoDetails } from '../../shared/video/video-details.model' 15import { VideoDetails } from '../../shared/video/video-details.model'
@@ -24,12 +19,11 @@ import { VideoReportComponent } from './modal/video-report.component'
24import { VideoShareComponent } from './modal/video-share.component' 19import { VideoShareComponent } from './modal/video-share.component'
25import { VideoBlacklistComponent } from './modal/video-blacklist.component' 20import { VideoBlacklistComponent } from './modal/video-blacklist.component'
26import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component' 21import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component'
27import { addContextMenu, getVideojsOptions, loadLocaleInVideoJS } from '../../../assets/player/peertube-player'
28import { I18n } from '@ngx-translate/i18n-polyfill' 22import { I18n } from '@ngx-translate/i18n-polyfill'
29import { environment } from '../../../environments/environment' 23import { environment } from '../../../environments/environment'
30import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
31import { VideoCaptionService } from '@app/shared/video-caption' 24import { VideoCaptionService } from '@app/shared/video-caption'
32import { MarkdownService } from '@app/shared/renderer' 25import { MarkdownService } from '@app/shared/renderer'
26import { PeertubePlayerManager } from '../../../assets/player/peertube-player-manager'
33 27
34@Component({ 28@Component({
35 selector: 'my-video-watch', 29 selector: 'my-video-watch',
@@ -46,7 +40,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
46 @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent 40 @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent
47 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent 41 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
48 42
49 player: videojs.Player 43 player: any
50 playerElement: HTMLVideoElement 44 playerElement: HTMLVideoElement
51 userRating: UserVideoRateType = null 45 userRating: UserVideoRateType = null
52 video: VideoDetails = null 46 video: VideoDetails = null
@@ -61,7 +55,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
61 remoteServerDown = false 55 remoteServerDown = false
62 hotkeys: Hotkey[] 56 hotkeys: Hotkey[]
63 57
64 private videojsLocaleLoaded = false
65 private paramsSub: Subscription 58 private paramsSub: Subscription
66 59
67 constructor ( 60 constructor (
@@ -402,41 +395,45 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
402 src: environment.apiUrl + c.captionPath 395 src: environment.apiUrl + c.captionPath
403 })) 396 }))
404 397
405 const videojsOptions = getVideojsOptions({ 398 const options = {
406 autoplay: this.isAutoplay(), 399 common: {
407 inactivityTimeout: 2500, 400 autoplay: this.isAutoplay(),
408 videoFiles: this.video.files, 401 playerElement: this.playerElement,
409 videoCaptions: playerCaptions, 402 videoDuration: this.video.duration,
410 playerElement: this.playerElement, 403 enableHotkeys: true,
411 videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null, 404 inactivityTimeout: 2500,
412 videoDuration: this.video.duration, 405 poster: this.video.previewUrl,
413 enableHotkeys: true, 406 startTime,
414 peertubeLink: false, 407
415 poster: this.video.previewUrl, 408 theaterMode: true,
416 startTime, 409 captions: videoCaptions.length !== 0,
417 subtitle: urlOptions.subtitle, 410 peertubeLink: false,
418 theaterMode: true, 411
419 language: this.localeId, 412 videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null,
420 413 embedUrl: this.video.embedUrl,
421 userWatching: this.user && this.user.videosHistoryEnabled === true ? { 414
422 url: this.videoService.getUserWatchingVideoUrl(this.video.uuid), 415 language: this.localeId,
423 authorizationHeader: this.authService.getRequestHeaderValue() 416
424 } : undefined 417 subtitle: urlOptions.subtitle,
425 })
426 418
427 if (this.videojsLocaleLoaded === false) { 419 userWatching: this.user && this.user.videosHistoryEnabled === true ? {
428 await loadLocaleInVideoJS(environment.apiUrl, videojs, isOnDevLocale() ? getDevLocale() : this.localeId) 420 url: this.videoService.getUserWatchingVideoUrl(this.video.uuid),
429 this.videojsLocaleLoaded = true 421 authorizationHeader: this.authService.getRequestHeaderValue()
422 } : undefined,
423
424 serverUrl: environment.apiUrl,
425
426 videoCaptions: playerCaptions
427 },
428
429 webtorrent: {
430 videoFiles: this.video.files
431 }
430 } 432 }
431 433
432 const self = this
433 this.zone.runOutsideAngular(async () => { 434 this.zone.runOutsideAngular(async () => {
434 videojs(this.playerElement, videojsOptions, function (this: videojs.Player) { 435 this.player = await PeertubePlayerManager.initialize('webtorrent', options)
435 self.player = this 436 this.player.on('customError', ({ err }: { err: any }) => this.handleError(err))
436 this.on('customError', ({ err }: { err: any }) => self.handleError(err))
437
438 addContextMenu(self.player, self.video.embedUrl)
439 })
440 }) 437 })
441 438
442 this.setVideoDescriptionHTML() 439 this.setVideoDescriptionHTML()
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
3import * as videojs from 'video.js'
4import { P2PMediaLoaderPluginOptions, VideoJSComponentInterface } from './peertube-videojs-typings'
5
6// videojs-hlsjs-plugin needs videojs in window
7window['videojs'] = videojs
8import '@streamroot/videojs-hlsjs-plugin'
9
10import { initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs'
11
12// import { Events } from '../p2p-media-loader/p2p-media-loader-core/lib'
13
14const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
15class 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
32videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin)
33export { 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 @@
1import { VideoFile } from '../../../../shared/models/videos'
2// @ts-ignore
3import * as videojs from 'video.js'
4import 'videojs-hotkeys'
5import 'videojs-dock'
6import 'videojs-contextmenu-ui'
7import 'videojs-contrib-quality-levels'
8import './peertube-plugin'
9import './videojs-components/peertube-link-button'
10import './videojs-components/resolution-menu-button'
11import './videojs-components/settings-menu-button'
12import './videojs-components/p2p-info-button'
13import './videojs-components/peertube-load-progress-bar'
14import './videojs-components/theater-button'
15import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions, videojsUntyped } from './peertube-videojs-typings'
16import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils'
17import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n'
18import { Engine } from 'p2p-media-loader-hlsjs'
19
20// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
21videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed'
22// Change Captions to Subtitles/CC
23videojsUntyped.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)
25videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' '
26
27type PlayerMode = 'webtorrent' | 'p2p-media-loader'
28
29type WebtorrentOptions = {
30 videoFiles: VideoFile[]
31}
32
33type P2PMediaLoaderOptions = {
34 playlistUrl: string
35}
36
37type 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
67export type PeertubePlayerManagerOptions = {
68 common: CommonOptions,
69 webtorrent?: WebtorrentOptions,
70 p2pMediaLoader?: P2PMediaLoaderOptions
71}
72
73export 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
386export {
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 @@
1import { VideoFile } from '../../../../shared/models/videos'
2
3import 'videojs-hotkeys'
4import 'videojs-dock'
5import 'videojs-contextmenu-ui'
6import './peertube-link-button'
7import './resolution-menu-button'
8import './settings-menu-button'
9import './webtorrent-info-button'
10import './peertube-videojs-plugin'
11import './peertube-load-progress-bar'
12import './theater-button'
13import { UserWatching, VideoJSCaption, videojsUntyped } from './peertube-videojs-typings'
14import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils'
15import { 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
19import { Player } from 'video.js'
20
21// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
22videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed'
23// Change Captions to Subtitles/CC
24videojsUntyped.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)
26videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' '
27
28function 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
144function 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
208function 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
241function 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}
266namespace loadLocaleInVideoJS {
267 export const cache: { [ path: string ]: any } = {}
268}
269
270function 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
285export {
286 getServerTranslations,
287 loadLocaleInVideoJS,
288 getVideojsOptions,
289 addContextMenu
290}
291
292// ############################################################################
293
294function 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
3import * as videojs from 'video.js'
4import './videojs-components/settings-menu-button'
5import { PeerTubePluginOptions, UserWatching, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
6import { isMobile, timeToInt } from './utils'
7import {
8 getStoredLastSubtitle,
9 getStoredMute,
10 getStoredVolume,
11 saveLastSubtitle,
12 saveMuteInStore,
13 saveVolumeInStore
14} from './peertube-player-local-storage'
15
16const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
17class 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
218videojs.registerPlugin('peertube', PeerTubePlugin)
219export { 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 @@
3import * as videojs from 'video.js' 3import * as videojs from 'video.js'
4 4
5import { VideoFile } from '../../../../shared/models/videos/video.model' 5import { VideoFile } from '../../../../shared/models/videos/video.model'
6import { PeerTubePlugin } from './peertube-videojs-plugin' 6import { PeerTubePlugin } from './peertube-plugin'
7import { WebTorrentPlugin } from './webtorrent-plugin'
7 8
8declare namespace videojs { 9declare 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
33type PeertubePluginOptions = { 35type 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
47type WebtorrentPluginOptions = {
48 playerElement: HTMLVideoElement
49
50 autoplay: boolean
51 videoDuration: number
52
53 videoFiles: VideoFile[]
54}
55
56type P2PMediaLoaderPluginOptions = {
57 type: string
58 src: string
59}
60
61type 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
47const videojsUntyped = videojs as any 70const videojsUntyped = videojs as any
48 71
72type LoadedQualityData = {
73 qualitySwitchCallback: Function,
74 qualityData: {
75 video: {
76 id: number
77 label: string
78 selected: boolean
79 }[]
80 }
81}
82
83type ResolutionUpdateData = {
84 auto: boolean,
85 resolutionId: number
86}
87
88type AutoResolutionUpdateData = {
89 possible: boolean
90}
91
49export { 92export {
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
3import { Player } from 'video.js'
4
5import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
6
7const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
8class 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}
65MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
66
67export { 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 @@
1import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 1import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
2import { bytes } from './utils' 2import { bytes } from '../utils'
3 3
4const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 4const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
5class WebtorrentInfoButton extends Button { 5class 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}
102Button.registerComponent('WebTorrentButton', WebtorrentInfoButton) 102Button.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 @@
1import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 1import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
2import { buildVideoLink } from './utils' 2import { 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
5import { Player } from 'video.js' 5import { 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 @@
1import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 1import { 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
4import { Player } from 'video.js' 4import { 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
3import { Player } from 'video.js' 3import { Player } from 'video.js'
4 4
5import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 5import { LoadedQualityData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
6import { ResolutionMenuItem } from './resolution-menu-item' 6import { ResolutionMenuItem } from './resolution-menu-item'
7 7
8const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') 8const 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}
86ResolutionMenuButton.prototype.controlText_ = 'Quality' 82ResolutionMenuButton.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
3import { Player } from 'video.js'
4
5import { AutoResolutionUpdateData, ResolutionUpdateData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
6
7const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
8class 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}
85MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
86
87export { 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 @@
6import * as videojs from 'video.js' 6import * as videojs from 'video.js'
7 7
8import { SettingsMenuItem } from './settings-menu-item' 8import { SettingsMenuItem } from './settings-menu-item'
9import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 9import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
10import { toTitleCase } from './utils' 10import { toTitleCase } from '../utils'
11 11
12const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 12const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
13const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') 13const 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
6import * as videojs from 'video.js' 6import * as videojs from 'video.js'
7 7
8import { toTitleCase } from './utils' 8import { toTitleCase } from '../utils'
9import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 9import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
10 10
11const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') 11const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
12const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') 12const 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
3import * as videojs from 'video.js' 3import * as videojs from 'video.js'
4 4
5import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 5import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
6import { saveTheaterInStore, getStoredTheater } from './peertube-player-local-storage' 6import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage'
7 7
8const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 8const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
9class TheaterButton extends Button { 9class 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
5import * as WebTorrent from 'webtorrent' 5import * as WebTorrent from 'webtorrent'
6import { VideoFile } from '../../../../shared/models/videos/video.model' 6import { VideoFile } from '../../../../shared/models/videos/video.model'
7import { renderVideo } from './video-renderer' 7import { renderVideo } from './webtorrent/video-renderer'
8import './settings-menu-button' 8import { LoadedQualityData, VideoJSComponentInterface, WebtorrentPluginOptions } from './peertube-videojs-typings'
9import { PeertubePluginOptions, UserWatching, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 9import { videoFileMaxByResolution, videoFileMinByResolution } from './utils'
10import { isMobile, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from './utils' 10import { PeertubeChunkStore } from './webtorrent/peertube-chunk-store'
11import { PeertubeChunkStore } from './peertube-chunk-store'
12import { 11import {
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
24const CacheChunkStore = require('cache-chunk-store') 19const CacheChunkStore = require('cache-chunk-store')
@@ -30,14 +25,13 @@ type PlayOptions = {
30} 25}
31 26
32const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') 27const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
33class PeerTubePlugin extends Plugin { 28class 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
753videojs.registerPlugin('peertube', PeerTubePlugin) 639videojs.registerPlugin('webtorrent', WebTorrentPlugin)
754export { PeerTubePlugin } 640export { 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
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index 54b8fb543..b1261c4a2 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -17,17 +17,13 @@ import 'core-js/es6/set'
17// For google bot that uses Chrome 41 and does not understand fetch 17// For google bot that uses Chrome 41 and does not understand fetch
18import 'whatwg-fetch' 18import 'whatwg-fetch'
19 19
20// FIXME: something weird with our path definition in tsconfig and typings
21// @ts-ignore
22import * as vjs from 'video.js'
23
24import * as Channel from 'jschannel' 20import * as Channel from 'jschannel'
25 21
26import { peertubeTranslate, ResultList, VideoDetails } from '../../../../shared' 22import { peertubeTranslate, ResultList, VideoDetails } from '../../../../shared'
27import { addContextMenu, getServerTranslations, getVideojsOptions, loadLocaleInVideoJS } from '../../assets/player/peertube-player'
28import { PeerTubeResolution } from '../player/definitions' 23import { PeerTubeResolution } from '../player/definitions'
29import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' 24import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
30import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' 25import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model'
26import { PeertubePlayerManager, PeertubePlayerManagerOptions } from '../../assets/player/peertube-player-manager'
31 27
32/** 28/**
33 * Embed API exposes control of the embed player to the outside world via 29 * Embed API exposes control of the embed player to the outside world via
@@ -73,16 +69,16 @@ class PeerTubeEmbedApi {
73 } 69 }
74 70
75 private setResolution (resolutionId: number) { 71 private setResolution (resolutionId: number) {
76 if (resolutionId === -1 && this.embed.player.peertube().isAutoResolutionForbidden()) return 72 if (resolutionId === -1 && this.embed.player.webtorrent().isAutoResolutionForbidden()) return
77 73
78 // Auto resolution 74 // Auto resolution
79 if (resolutionId === -1) { 75 if (resolutionId === -1) {
80 this.embed.player.peertube().enableAutoResolution() 76 this.embed.player.webtorrent().enableAutoResolution()
81 return 77 return
82 } 78 }
83 79
84 this.embed.player.peertube().disableAutoResolution() 80 this.embed.player.webtorrent().disableAutoResolution()
85 this.embed.player.peertube().updateResolution(resolutionId) 81 this.embed.player.webtorrent().updateResolution(resolutionId)
86 } 82 }
87 83
88 /** 84 /**
@@ -122,15 +118,17 @@ class PeerTubeEmbedApi {
122 118
123 // PeerTube specific capabilities 119 // PeerTube specific capabilities
124 120
125 this.embed.player.peertube().on('autoResolutionUpdate', () => this.loadResolutions()) 121 if (this.embed.player.webtorrent) {
126 this.embed.player.peertube().on('videoFileUpdate', () => this.loadResolutions()) 122 this.embed.player.webtorrent().on('autoResolutionUpdate', () => this.loadWebTorrentResolutions())
123 this.embed.player.webtorrent().on('videoFileUpdate', () => this.loadWebTorrentResolutions())
124 }
127 } 125 }
128 126
129 private loadResolutions () { 127 private loadWebTorrentResolutions () {
130 let resolutions = [] 128 let resolutions = []
131 let currentResolutionId = this.embed.player.peertube().getCurrentResolutionId() 129 let currentResolutionId = this.embed.player.webtorrent().getCurrentResolutionId()
132 130
133 for (const videoFile of this.embed.player.peertube().videoFiles) { 131 for (const videoFile of this.embed.player.webtorrent().videoFiles) {
134 let label = videoFile.resolution.label 132 let label = videoFile.resolution.label
135 if (videoFile.fps && videoFile.fps >= 50) { 133 if (videoFile.fps && videoFile.fps >= 50) {
136 label += videoFile.fps 134 label += videoFile.fps
@@ -266,9 +264,8 @@ class PeerTubeEmbed {
266 const urlParts = window.location.pathname.split('/') 264 const urlParts = window.location.pathname.split('/')
267 const videoId = urlParts[ urlParts.length - 1 ] 265 const videoId = urlParts[ urlParts.length - 1 ]
268 266
269 const [ , serverTranslations, videoResponse, captionsResponse ] = await Promise.all([ 267 const [ serverTranslations, videoResponse, captionsResponse ] = await Promise.all([
270 loadLocaleInVideoJS(window.location.origin, vjs, navigator.language), 268 PeertubePlayerManager.getServerTranslations(window.location.origin, navigator.language),
271 getServerTranslations(window.location.origin, navigator.language),
272 this.loadVideoInfo(videoId), 269 this.loadVideoInfo(videoId),
273 this.loadVideoCaptions(videoId) 270 this.loadVideoCaptions(videoId)
274 ]) 271 ])
@@ -292,43 +289,56 @@ class PeerTubeEmbed {
292 289
293 this.loadParams() 290 this.loadParams()
294 291
295 const videojsOptions = getVideojsOptions({ 292 const options: PeertubePlayerManagerOptions = {
296 autoplay: this.autoplay, 293 common: {
297 controls: this.controls, 294 autoplay: this.autoplay,
298 muted: this.muted, 295 controls: this.controls,
299 loop: this.loop, 296 muted: this.muted,
300 startTime: this.startTime, 297 loop: this.loop,
301 subtitle: this.subtitle, 298 captions: videoCaptions.length !== 0,
302 299 startTime: this.startTime,
303 videoCaptions, 300 subtitle: this.subtitle,
304 inactivityTimeout: 1500, 301
305 videoViewUrl: this.getVideoUrl(videoId) + '/views', 302 videoCaptions,
306 playerElement: this.videoElement, 303 inactivityTimeout: 1500,
307 videoFiles: videoInfo.files, 304 videoViewUrl: this.getVideoUrl(videoId) + '/views',
308 videoDuration: videoInfo.duration, 305 playerElement: this.videoElement,
309 enableHotkeys: true, 306 videoDuration: videoInfo.duration,
310 peertubeLink: true, 307 enableHotkeys: true,
311 poster: window.location.origin + videoInfo.previewPath, 308 peertubeLink: true,
312 theaterMode: false 309 poster: window.location.origin + videoInfo.previewPath,
313 }) 310 theaterMode: false,
311
312 serverUrl: window.location.origin,
313 language: navigator.language,
314 embedUrl: window.location.origin + videoInfo.embedPath
315 },
316
317 webtorrent: {
318 videoFiles: videoInfo.files
319 }
320
321 // p2pMediaLoader: {
322 // // playlistUrl: 'https://akamai-axtest.akamaized.net/routes/lapd-v1-acceptance/www_c4/Manifest.m3u8'
323 // // playlistUrl: 'https://d2zihajmogu5jn.cloudfront.net/bipbop-advanced/bipbop_16x9_variant.m3u8'
324 // playlistUrl: 'https://cdn.theoplayer.com/video/elephants-dream/playlist.m3u8'
325 // }
326 }
314 327
315 this.playerOptions = videojsOptions 328 this.player = await PeertubePlayerManager.initialize('webtorrent', options)
316 this.player = vjs(this.videoContainerId, videojsOptions, () => {
317 this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations))
318 329
319 window[ 'videojsPlayer' ] = this.player 330 this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations))
320 331
321 if (this.controls) { 332 window[ 'videojsPlayer' ] = this.player
322 this.player.dock({
323 title: videoInfo.name,
324 description: this.player.localize('Uses P2P, others may know your IP is downloading this video.')
325 })
326 }
327 333
328 addContextMenu(this.player, window.location.origin + videoInfo.embedPath) 334 if (this.controls) {
335 this.player.dock({
336 title: videoInfo.name,
337 description: this.player.localize('Uses P2P, others may know your IP is downloading this video.')
338 })
339 }
329 340
330 this.initializeApi() 341 this.initializeApi()
331 })
332 } 342 }
333 343
334 private handleError (err: Error, translations?: { [ id: string ]: string }) { 344 private handleError (err: Error, translations?: { [ id: string ]: string }) {
diff --git a/client/src/tsconfig.app.json b/client/src/tsconfig.app.json
index af7a74e9e..729eee353 100644
--- a/client/src/tsconfig.app.json
+++ b/client/src/tsconfig.app.json
@@ -3,7 +3,7 @@
3 "compilerOptions": { 3 "compilerOptions": {
4 "outDir": "../out-tsc/app", 4 "outDir": "../out-tsc/app",
5 "baseUrl": "./", 5 "baseUrl": "./",
6 "module": "es2015", 6 "module": "esnext",
7 "types": [], 7 "types": [],
8 "lib": [ 8 "lib": [
9 "es2017", 9 "es2017",