]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - client/src/assets/player/shared/peertube/peertube-plugin.ts
feat: show contained playlists under My videos (#5125)
[github/Chocobozzz/PeerTube.git] / client / src / assets / player / shared / peertube / peertube-plugin.ts
CommitLineData
9af2acce 1import debug from 'debug'
15a7eafb 2import videojs from 'video.js'
42b40636 3import { logger } from '@root-helpers/logger'
57d65032 4import { isMobile } from '@root-helpers/web-browser'
15a7eafb 5import { timeToInt } from '@shared/core-utils'
384ba8b7 6import { VideoView, VideoViewEvent } from '@shared/models/videos'
2adfc7ea
C
7import {
8 getStoredLastSubtitle,
9 getStoredMute,
10 getStoredVolume,
11 saveLastSubtitle,
12 saveMuteInStore,
58b9ce30 13 saveVideoWatchHistory,
2adfc7ea 14 saveVolumeInStore
57d65032 15} from '../../peertube-player-local-storage'
384ba8b7 16import { PeerTubePluginOptions, VideoJSCaption } from '../../types'
57d65032 17import { SettingsButton } from '../settings/settings-menu-button'
f1a0555a 18
42b40636 19const debugLogger = debug('peertube:player:peertube')
2adfc7ea 20
f5fcd9f7
C
21const Plugin = videojs.getPlugin('plugin')
22
2adfc7ea 23class PeerTubePlugin extends Plugin {
2adfc7ea 24 private readonly videoViewUrl: string
384ba8b7
C
25 private readonly authorizationHeader: string
26
27 private readonly videoUUID: string
28 private readonly startTime: number
29
2adfc7ea 30 private readonly CONSTANTS = {
384ba8b7 31 USER_VIEW_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
2adfc7ea
C
32 }
33
2adfc7ea
C
34 private videoCaptions: VideoJSCaption[]
35 private defaultSubtitle: string
36
37 private videoViewInterval: any
10f26f42 38
d1f21ebb
C
39 private menuOpened = false
40 private mouseInControlBar = false
dc9ff312
C
41 private mouseInSettings = false
42 private readonly initialInactivityTimeout: number
d1f21ebb 43
7e37e111 44 constructor (player: videojs.Player, options?: PeerTubePluginOptions) {
f5fcd9f7 45 super(player)
2adfc7ea 46
2adfc7ea 47 this.videoViewUrl = options.videoViewUrl
384ba8b7
C
48 this.authorizationHeader = options.authorizationHeader
49 this.videoUUID = options.videoUUID
50 this.startTime = timeToInt(options.startTime)
51
2adfc7ea 52 this.videoCaptions = options.videoCaptions
dc9ff312 53 this.initialInactivityTimeout = this.player.options_.inactivityTimeout
d1f21ebb 54
72efdda5 55 if (options.autoplay) this.player.addClass('vjs-has-autoplay')
6ec0b75b
C
56
57 this.player.on('autoplay-failure', () => {
58 this.player.removeClass('vjs-has-autoplay')
59 })
2adfc7ea
C
60
61 this.player.ready(() => {
62 const playerOptions = this.player.options_
63
64 const volume = getStoredVolume()
65 if (volume !== undefined) this.player.volume(volume)
66
67 const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
68 if (muted !== undefined) this.player.muted(muted)
69
70 this.defaultSubtitle = options.subtitle || getStoredLastSubtitle()
71
72 this.player.on('volumechange', () => {
73 saveVolumeInStore(this.player.volume())
74 saveMuteInStore(this.player.muted())
75 })
76
f0a39880
C
77 if (options.stopTime) {
78 const stopTime = timeToInt(options.stopTime)
e2f01c47 79 const self = this
f0a39880 80
e2f01c47
C
81 this.player.on('timeupdate', function onTimeUpdate () {
82 if (self.player.currentTime() > stopTime) {
83 self.player.pause()
84 self.player.trigger('stopped')
85
86 self.player.off('timeupdate', onTimeUpdate)
87 }
f0a39880
C
88 })
89 }
90
e367da94 91 this.player.textTracks().addEventListener('change', () => {
f5fcd9f7 92 const showing = this.player.textTracks().tracks_.find(t => {
2adfc7ea
C
93 return t.kind === 'captions' && t.mode === 'showing'
94 })
95
96 if (!showing) {
97 saveLastSubtitle('off')
98 return
99 }
100
101 saveLastSubtitle(showing.language)
102 })
103
104 this.player.on('sourcechange', () => this.initCaptions())
105
106 this.player.duration(options.videoDuration)
107
108 this.initializePlayer()
384ba8b7 109 this.runUserViewing()
2adfc7ea
C
110 })
111 }
112
113 dispose () {
f0a39880 114 if (this.videoViewInterval) clearInterval(this.videoViewInterval)
2adfc7ea
C
115 }
116
dc9ff312
C
117 onMenuOpened () {
118 this.menuOpened = true
d1f21ebb
C
119 this.alterInactivity()
120 }
121
122 onMenuClosed () {
dc9ff312 123 this.menuOpened = false
d1f21ebb
C
124 this.alterInactivity()
125 }
126
c4207f97 127 displayFatalError () {
f2a16d93 128 this.player.loadingSpinner.hide()
129
130 const buildModal = (error: MediaError) => {
131 const localize = this.player.localize.bind(this.player)
132
133 const wrapper = document.createElement('div')
134 const header = document.createElement('h1')
135 header.innerText = localize('Failed to play video')
136 wrapper.appendChild(header)
137 const desc = document.createElement('div')
138 desc.innerText = localize('The video failed to play due to technical issues.')
139 wrapper.appendChild(desc)
140 const details = document.createElement('p')
141 details.classList.add('error-details')
142 details.innerText = error.message
143 wrapper.appendChild(details)
144
145 return wrapper
146 }
147
148 const modal = this.player.createModal(buildModal(this.player.error()), {
149 temporary: false,
150 uncloseable: true
151 })
152 modal.addClass('vjs-custom-error-display')
153
c4207f97
C
154 this.player.addClass('vjs-error-display-enabled')
155 }
156
157 hideFatalError () {
158 this.player.removeClass('vjs-error-display-enabled')
159 }
160
2adfc7ea
C
161 private initializePlayer () {
162 if (isMobile()) this.player.addClass('vjs-is-mobile')
163
164 this.initSmoothProgressBar()
165
166 this.initCaptions()
167
d1f21ebb 168 this.listenControlBarMouse()
07d6044e
C
169
170 this.listenFullScreenChange()
2adfc7ea
C
171 }
172
fd3c2e87
C
173 // ---------------------------------------------------------------------------
174
384ba8b7
C
175 private runUserViewing () {
176 let lastCurrentTime = this.startTime
177 let lastViewEvent: VideoViewEvent
2adfc7ea 178
384ba8b7
C
179 this.player.one('play', () => {
180 this.notifyUserIsWatching(this.startTime, lastViewEvent)
181 })
2adfc7ea 182
384ba8b7
C
183 this.player.on('seeked', () => {
184 // Don't take into account small seek events
185 if (Math.abs(this.player.currentTime() - lastCurrentTime) < 3) return
2adfc7ea 186
384ba8b7
C
187 lastViewEvent = 'seek'
188 })
2adfc7ea 189
384ba8b7 190 this.player.one('ended', () => {
1222a602 191 const currentTime = Math.round(this.player.duration())
384ba8b7 192 lastCurrentTime = currentTime
2adfc7ea 193
384ba8b7 194 this.notifyUserIsWatching(currentTime, lastViewEvent)
2adfc7ea 195
384ba8b7
C
196 lastViewEvent = undefined
197 })
198
199 this.videoViewInterval = setInterval(() => {
1222a602 200 const currentTime = Math.round(this.player.currentTime())
2adfc7ea 201
384ba8b7
C
202 // No need to update
203 if (currentTime === lastCurrentTime) return
2adfc7ea 204
384ba8b7 205 lastCurrentTime = currentTime
2adfc7ea 206
384ba8b7 207 this.notifyUserIsWatching(currentTime, lastViewEvent)
42b40636 208 .catch(err => logger.error('Cannot notify user is watching.', err))
384ba8b7
C
209
210 lastViewEvent = undefined
211
212 // Server won't save history, so save the video position in local storage
213 if (!this.authorizationHeader) {
214 saveVideoWatchHistory(this.videoUUID, currentTime)
215 }
216 }, this.CONSTANTS.USER_VIEW_VIDEO_INTERVAL)
2adfc7ea
C
217 }
218
384ba8b7 219 private notifyUserIsWatching (currentTime: number, viewEvent: VideoViewEvent) {
2adfc7ea
C
220 if (!this.videoViewUrl) return Promise.resolve(undefined)
221
384ba8b7
C
222 const body: VideoView = {
223 currentTime,
224 viewEvent
225 }
2adfc7ea 226
384ba8b7
C
227 const headers = new Headers({
228 'Content-type': 'application/json; charset=UTF-8'
229 })
2adfc7ea 230
384ba8b7 231 if (this.authorizationHeader) headers.set('Authorization', this.authorizationHeader)
2adfc7ea 232
384ba8b7 233 return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers })
2adfc7ea
C
234 }
235
fd3c2e87
C
236 // ---------------------------------------------------------------------------
237
07d6044e
C
238 private listenFullScreenChange () {
239 this.player.on('fullscreenchange', () => {
240 if (this.player.isFullscreen()) this.player.focus()
241 })
242 }
243
d1f21ebb 244 private listenControlBarMouse () {
dc9ff312
C
245 const controlBar = this.player.controlBar
246 const settingsButton: SettingsButton = (controlBar as any).settingsButton
247
248 controlBar.on('mouseenter', () => {
d1f21ebb
C
249 this.mouseInControlBar = true
250 this.alterInactivity()
251 })
2adfc7ea 252
dc9ff312 253 controlBar.on('mouseleave', () => {
d1f21ebb
C
254 this.mouseInControlBar = false
255 this.alterInactivity()
256 })
dc9ff312
C
257
258 settingsButton.dialog.on('mouseenter', () => {
259 this.mouseInSettings = true
260 this.alterInactivity()
261 })
262
263 settingsButton.dialog.on('mouseleave', () => {
264 this.mouseInSettings = false
265 this.alterInactivity()
266 })
d1f21ebb 267 }
2adfc7ea 268
d1f21ebb 269 private alterInactivity () {
f1a0555a 270 if (this.menuOpened || this.mouseInSettings || this.mouseInControlBar) {
dc9ff312 271 this.setInactivityTimeout(0)
d1f21ebb
C
272 return
273 }
2adfc7ea 274
dc9ff312
C
275 this.setInactivityTimeout(this.initialInactivityTimeout)
276 this.player.reportUserActivity(true)
277 }
278
279 private setInactivityTimeout (timeout: number) {
280 (this.player as any).cache_.inactivityTimeout = timeout
281 this.player.options_.inactivityTimeout = timeout
f1a0555a 282
42b40636 283 debugLogger('Set player inactivity to ' + timeout)
35f0a5e6
C
284 }
285
2adfc7ea
C
286 private initCaptions () {
287 for (const caption of this.videoCaptions) {
288 this.player.addRemoteTextTrack({
289 kind: 'captions',
290 label: caption.label,
291 language: caption.language,
292 id: caption.language,
293 src: caption.src,
294 default: this.defaultSubtitle === caption.language
295 }, false)
296 }
297
298 this.player.trigger('captionsChanged')
299 }
300
301 // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
302 private initSmoothProgressBar () {
f5fcd9f7 303 const SeekBar = videojs.getComponent('SeekBar') as any
2adfc7ea
C
304 SeekBar.prototype.getPercent = function getPercent () {
305 // Allows for smooth scrubbing, when player can't keep up.
306 // const time = (this.player_.scrubbing()) ?
307 // this.player_.getCache().currentTime :
308 // this.player_.currentTime()
309 const time = this.player_.currentTime()
310 const percent = time / this.player_.duration()
311 return percent >= 1 ? 1 : percent
312 }
313 SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) {
314 let newTime = this.calculateDistance(event) * this.player_.duration()
315 if (newTime === this.player_.duration()) {
316 newTime = newTime - 0.1
317 }
318 this.player_.currentTime(newTime)
319 this.update()
320 }
321 }
322}
323
324videojs.registerPlugin('peertube', PeerTubePlugin)
325export { PeerTubePlugin }