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