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