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