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