]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - client/src/assets/player/peertube-plugin.ts
Merge branch 'release/4.1.0' into develop
[github/Chocobozzz/PeerTube.git] / client / src / assets / player / peertube-plugin.ts
CommitLineData
15a7eafb
C
1import videojs from 'video.js'
2import { timeToInt } from '@shared/core-utils'
2adfc7ea
C
3import {
4 getStoredLastSubtitle,
5 getStoredMute,
6 getStoredVolume,
7 saveLastSubtitle,
8 saveMuteInStore,
58b9ce30 9 saveVideoWatchHistory,
2adfc7ea
C
10 saveVolumeInStore
11} from './peertube-player-local-storage'
e367da94 12import { PeerTubePluginOptions, UserWatching, VideoJSCaption } from './peertube-videojs-typings'
15a7eafb 13import { isMobile } from './utils'
dc9ff312 14import { SettingsButton } from './videojs-components/settings-menu-button'
f1a0555a
C
15import debug from 'debug'
16
17const logger = debug('peertube:player:peertube')
2adfc7ea 18
f5fcd9f7
C
19const Plugin = videojs.getPlugin('plugin')
20
2adfc7ea 21class PeerTubePlugin extends Plugin {
2adfc7ea
C
22 private readonly videoViewUrl: string
23 private readonly videoDuration: number
24 private readonly CONSTANTS = {
25 USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
26 }
27
2adfc7ea
C
28 private videoCaptions: VideoJSCaption[]
29 private defaultSubtitle: string
30
31 private videoViewInterval: any
32 private userWatchingVideoInterval: any
2adfc7ea 33
10f26f42
C
34 private isLive: boolean
35
d1f21ebb
C
36 private menuOpened = false
37 private mouseInControlBar = false
dc9ff312
C
38 private mouseInSettings = false
39 private readonly initialInactivityTimeout: number
d1f21ebb 40
7e37e111 41 constructor (player: videojs.Player, options?: PeerTubePluginOptions) {
f5fcd9f7 42 super(player)
2adfc7ea 43
2adfc7ea
C
44 this.videoViewUrl = options.videoViewUrl
45 this.videoDuration = options.videoDuration
46 this.videoCaptions = options.videoCaptions
10f26f42 47 this.isLive = options.isLive
dc9ff312 48 this.initialInactivityTimeout = this.player.options_.inactivityTimeout
d1f21ebb 49
72efdda5 50 if (options.autoplay) this.player.addClass('vjs-has-autoplay')
6ec0b75b
C
51
52 this.player.on('autoplay-failure', () => {
53 this.player.removeClass('vjs-has-autoplay')
54 })
2adfc7ea
C
55
56 this.player.ready(() => {
57 const playerOptions = this.player.options_
58
59 const volume = getStoredVolume()
60 if (volume !== undefined) this.player.volume(volume)
61
62 const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
63 if (muted !== undefined) this.player.muted(muted)
64
65 this.defaultSubtitle = options.subtitle || getStoredLastSubtitle()
66
67 this.player.on('volumechange', () => {
68 saveVolumeInStore(this.player.volume())
69 saveMuteInStore(this.player.muted())
70 })
71
f0a39880
C
72 if (options.stopTime) {
73 const stopTime = timeToInt(options.stopTime)
e2f01c47 74 const self = this
f0a39880 75
e2f01c47
C
76 this.player.on('timeupdate', function onTimeUpdate () {
77 if (self.player.currentTime() > stopTime) {
78 self.player.pause()
79 self.player.trigger('stopped')
80
81 self.player.off('timeupdate', onTimeUpdate)
82 }
f0a39880
C
83 })
84 }
85
e367da94 86 this.player.textTracks().addEventListener('change', () => {
f5fcd9f7 87 const showing = this.player.textTracks().tracks_.find(t => {
2adfc7ea
C
88 return t.kind === 'captions' && t.mode === 'showing'
89 })
90
91 if (!showing) {
92 saveLastSubtitle('off')
93 return
94 }
95
96 saveLastSubtitle(showing.language)
97 })
98
99 this.player.on('sourcechange', () => this.initCaptions())
100
101 this.player.duration(options.videoDuration)
102
103 this.initializePlayer()
104 this.runViewAdd()
105
58b9ce30 106 this.runUserWatchVideo(options.userWatching, options.videoUUID)
2adfc7ea
C
107 })
108 }
109
110 dispose () {
f0a39880 111 if (this.videoViewInterval) clearInterval(this.videoViewInterval)
2adfc7ea
C
112 if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
113 }
114
dc9ff312
C
115 onMenuOpened () {
116 this.menuOpened = true
d1f21ebb
C
117 this.alterInactivity()
118 }
119
120 onMenuClosed () {
dc9ff312 121 this.menuOpened = false
d1f21ebb
C
122 this.alterInactivity()
123 }
124
c4207f97
C
125 displayFatalError () {
126 this.player.addClass('vjs-error-display-enabled')
127 }
128
129 hideFatalError () {
130 this.player.removeClass('vjs-error-display-enabled')
131 }
132
2adfc7ea
C
133 private initializePlayer () {
134 if (isMobile()) this.player.addClass('vjs-is-mobile')
135
136 this.initSmoothProgressBar()
137
138 this.initCaptions()
139
d1f21ebb 140 this.listenControlBarMouse()
07d6044e
C
141
142 this.listenFullScreenChange()
2adfc7ea
C
143 }
144
145 private runViewAdd () {
146 this.clearVideoViewInterval()
147
148 // After 30 seconds (or 3/4 of the video), add a view to the video
149 let minSecondsToView = 30
150
10f26f42
C
151 if (!this.isLive && this.videoDuration < minSecondsToView) {
152 minSecondsToView = (this.videoDuration * 3) / 4
153 }
2adfc7ea
C
154
155 let secondsViewed = 0
156 this.videoViewInterval = setInterval(() => {
157 if (this.player && !this.player.paused()) {
158 secondsViewed += 1
159
160 if (secondsViewed > minSecondsToView) {
10f26f42
C
161 // Restart the loop if this is a live
162 if (this.isLive) {
163 secondsViewed = 0
164 } else {
165 this.clearVideoViewInterval()
166 }
2adfc7ea
C
167
168 this.addViewToVideo().catch(err => console.error(err))
169 }
170 }
171 }, 1000)
172 }
173
58b9ce30 174 private runUserWatchVideo (options: UserWatching, videoUUID: string) {
2adfc7ea
C
175 let lastCurrentTime = 0
176
177 this.userWatchingVideoInterval = setInterval(() => {
178 const currentTime = Math.floor(this.player.currentTime())
179
180 if (currentTime - lastCurrentTime >= 1) {
181 lastCurrentTime = currentTime
182
58b9ce30 183 if (options) {
184 this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader)
185 .catch(err => console.error('Cannot notify user is watching.', err))
186 } else {
187 saveVideoWatchHistory(videoUUID, currentTime)
188 }
2adfc7ea
C
189 }
190 }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL)
191 }
192
193 private clearVideoViewInterval () {
194 if (this.videoViewInterval !== undefined) {
195 clearInterval(this.videoViewInterval)
196 this.videoViewInterval = undefined
197 }
198 }
199
200 private addViewToVideo () {
201 if (!this.videoViewUrl) return Promise.resolve(undefined)
202
203 return fetch(this.videoViewUrl, { method: 'POST' })
204 }
205
206 private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) {
207 const body = new URLSearchParams()
208 body.append('currentTime', currentTime.toString())
209
9df52d66 210 const headers = new Headers({ Authorization: authorizationHeader })
2adfc7ea
C
211
212 return fetch(url, { method: 'PUT', body, headers })
213 }
214
07d6044e
C
215 private listenFullScreenChange () {
216 this.player.on('fullscreenchange', () => {
217 if (this.player.isFullscreen()) this.player.focus()
218 })
219 }
220
d1f21ebb 221 private listenControlBarMouse () {
dc9ff312
C
222 const controlBar = this.player.controlBar
223 const settingsButton: SettingsButton = (controlBar as any).settingsButton
224
225 controlBar.on('mouseenter', () => {
d1f21ebb
C
226 this.mouseInControlBar = true
227 this.alterInactivity()
228 })
2adfc7ea 229
dc9ff312 230 controlBar.on('mouseleave', () => {
d1f21ebb
C
231 this.mouseInControlBar = false
232 this.alterInactivity()
233 })
dc9ff312
C
234
235 settingsButton.dialog.on('mouseenter', () => {
236 this.mouseInSettings = true
237 this.alterInactivity()
238 })
239
240 settingsButton.dialog.on('mouseleave', () => {
241 this.mouseInSettings = false
242 this.alterInactivity()
243 })
d1f21ebb 244 }
2adfc7ea 245
d1f21ebb 246 private alterInactivity () {
f1a0555a 247 if (this.menuOpened || this.mouseInSettings || this.mouseInControlBar) {
dc9ff312 248 this.setInactivityTimeout(0)
d1f21ebb
C
249 return
250 }
2adfc7ea 251
dc9ff312
C
252 this.setInactivityTimeout(this.initialInactivityTimeout)
253 this.player.reportUserActivity(true)
254 }
255
256 private setInactivityTimeout (timeout: number) {
257 (this.player as any).cache_.inactivityTimeout = timeout
258 this.player.options_.inactivityTimeout = timeout
f1a0555a
C
259
260 logger('Set player inactivity to ' + timeout)
35f0a5e6
C
261 }
262
2adfc7ea
C
263 private initCaptions () {
264 for (const caption of this.videoCaptions) {
265 this.player.addRemoteTextTrack({
266 kind: 'captions',
267 label: caption.label,
268 language: caption.language,
269 id: caption.language,
270 src: caption.src,
271 default: this.defaultSubtitle === caption.language
272 }, false)
273 }
274
275 this.player.trigger('captionsChanged')
276 }
277
278 // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
279 private initSmoothProgressBar () {
f5fcd9f7 280 const SeekBar = videojs.getComponent('SeekBar') as any
2adfc7ea
C
281 SeekBar.prototype.getPercent = function getPercent () {
282 // Allows for smooth scrubbing, when player can't keep up.
283 // const time = (this.player_.scrubbing()) ?
284 // this.player_.getCache().currentTime :
285 // this.player_.currentTime()
286 const time = this.player_.currentTime()
287 const percent = time / this.player_.duration()
288 return percent >= 1 ? 1 : percent
289 }
290 SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) {
291 let newTime = this.calculateDistance(event) * this.player_.duration()
292 if (newTime === this.player_.duration()) {
293 newTime = newTime - 0.1
294 }
295 this.player_.currentTime(newTime)
296 this.update()
297 }
298 }
299}
300
301videojs.registerPlugin('peertube', PeerTubePlugin)
302export { PeerTubePlugin }