]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - client/src/assets/player/shared/peertube/peertube-plugin.ts
Support videos stats in client
[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'
57d65032 3import { isMobile } from '@root-helpers/web-browser'
15a7eafb 4import { timeToInt } from '@shared/core-utils'
384ba8b7 5import { VideoView, VideoViewEvent } from '@shared/models/videos'
2adfc7ea
C
6import {
7 getStoredLastSubtitle,
8 getStoredMute,
9 getStoredVolume,
10 saveLastSubtitle,
11 saveMuteInStore,
58b9ce30 12 saveVideoWatchHistory,
2adfc7ea 13 saveVolumeInStore
57d65032 14} from '../../peertube-player-local-storage'
384ba8b7 15import { PeerTubePluginOptions, VideoJSCaption } from '../../types'
57d65032 16import { SettingsButton } from '../settings/settings-menu-button'
f1a0555a
C
17
18const logger = debug('peertube:player:peertube')
2adfc7ea 19
f5fcd9f7
C
20const Plugin = videojs.getPlugin('plugin')
21
2adfc7ea 22class PeerTubePlugin extends Plugin {
2adfc7ea 23 private readonly videoViewUrl: string
384ba8b7
C
24 private readonly authorizationHeader: string
25
26 private readonly videoUUID: string
27 private readonly startTime: number
28
2adfc7ea 29 private readonly CONSTANTS = {
384ba8b7 30 USER_VIEW_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
2adfc7ea
C
31 }
32
2adfc7ea
C
33 private videoCaptions: VideoJSCaption[]
34 private defaultSubtitle: string
35
36 private videoViewInterval: any
10f26f42 37
d1f21ebb
C
38 private menuOpened = false
39 private mouseInControlBar = false
dc9ff312
C
40 private mouseInSettings = false
41 private readonly initialInactivityTimeout: number
d1f21ebb 42
7e37e111 43 constructor (player: videojs.Player, options?: PeerTubePluginOptions) {
f5fcd9f7 44 super(player)
2adfc7ea 45
2adfc7ea 46 this.videoViewUrl = options.videoViewUrl
384ba8b7
C
47 this.authorizationHeader = options.authorizationHeader
48 this.videoUUID = options.videoUUID
49 this.startTime = timeToInt(options.startTime)
50
2adfc7ea 51 this.videoCaptions = options.videoCaptions
dc9ff312 52 this.initialInactivityTimeout = this.player.options_.inactivityTimeout
d1f21ebb 53
72efdda5 54 if (options.autoplay) 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
C
126 displayFatalError () {
127 this.player.addClass('vjs-error-display-enabled')
128 }
129
130 hideFatalError () {
131 this.player.removeClass('vjs-error-display-enabled')
132 }
133
2adfc7ea
C
134 private initializePlayer () {
135 if (isMobile()) this.player.addClass('vjs-is-mobile')
136
137 this.initSmoothProgressBar()
138
139 this.initCaptions()
140
d1f21ebb 141 this.listenControlBarMouse()
07d6044e
C
142
143 this.listenFullScreenChange()
2adfc7ea
C
144 }
145
384ba8b7
C
146 private runUserViewing () {
147 let lastCurrentTime = this.startTime
148 let lastViewEvent: VideoViewEvent
2adfc7ea 149
384ba8b7
C
150 this.player.one('play', () => {
151 this.notifyUserIsWatching(this.startTime, lastViewEvent)
152 })
2adfc7ea 153
384ba8b7
C
154 this.player.on('seeked', () => {
155 // Don't take into account small seek events
156 if (Math.abs(this.player.currentTime() - lastCurrentTime) < 3) return
2adfc7ea 157
384ba8b7
C
158 lastViewEvent = 'seek'
159 })
2adfc7ea 160
384ba8b7
C
161 this.player.one('ended', () => {
162 const currentTime = Math.floor(this.player.duration())
163 lastCurrentTime = currentTime
2adfc7ea 164
384ba8b7 165 this.notifyUserIsWatching(currentTime, lastViewEvent)
2adfc7ea 166
384ba8b7
C
167 lastViewEvent = undefined
168 })
169
170 this.videoViewInterval = setInterval(() => {
2adfc7ea
C
171 const currentTime = Math.floor(this.player.currentTime())
172
384ba8b7
C
173 // No need to update
174 if (currentTime === lastCurrentTime) return
2adfc7ea 175
384ba8b7 176 lastCurrentTime = currentTime
2adfc7ea 177
384ba8b7
C
178 this.notifyUserIsWatching(currentTime, lastViewEvent)
179 .catch(err => console.error('Cannot notify user is watching.', err))
180
181 lastViewEvent = undefined
182
183 // Server won't save history, so save the video position in local storage
184 if (!this.authorizationHeader) {
185 saveVideoWatchHistory(this.videoUUID, currentTime)
186 }
187 }, this.CONSTANTS.USER_VIEW_VIDEO_INTERVAL)
2adfc7ea
C
188 }
189
384ba8b7 190 private notifyUserIsWatching (currentTime: number, viewEvent: VideoViewEvent) {
2adfc7ea
C
191 if (!this.videoViewUrl) return Promise.resolve(undefined)
192
384ba8b7
C
193 const body: VideoView = {
194 currentTime,
195 viewEvent
196 }
2adfc7ea 197
384ba8b7
C
198 const headers = new Headers({
199 'Content-type': 'application/json; charset=UTF-8'
200 })
2adfc7ea 201
384ba8b7 202 if (this.authorizationHeader) headers.set('Authorization', this.authorizationHeader)
2adfc7ea 203
384ba8b7 204 return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers })
2adfc7ea
C
205 }
206
07d6044e
C
207 private listenFullScreenChange () {
208 this.player.on('fullscreenchange', () => {
209 if (this.player.isFullscreen()) this.player.focus()
210 })
211 }
212
d1f21ebb 213 private listenControlBarMouse () {
dc9ff312
C
214 const controlBar = this.player.controlBar
215 const settingsButton: SettingsButton = (controlBar as any).settingsButton
216
217 controlBar.on('mouseenter', () => {
d1f21ebb
C
218 this.mouseInControlBar = true
219 this.alterInactivity()
220 })
2adfc7ea 221
dc9ff312 222 controlBar.on('mouseleave', () => {
d1f21ebb
C
223 this.mouseInControlBar = false
224 this.alterInactivity()
225 })
dc9ff312
C
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 })
d1f21ebb 236 }
2adfc7ea 237
d1f21ebb 238 private alterInactivity () {
f1a0555a 239 if (this.menuOpened || this.mouseInSettings || this.mouseInControlBar) {
dc9ff312 240 this.setInactivityTimeout(0)
d1f21ebb
C
241 return
242 }
2adfc7ea 243
dc9ff312
C
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
f1a0555a
C
251
252 logger('Set player inactivity to ' + timeout)
35f0a5e6
C
253 }
254
2adfc7ea
C
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 () {
f5fcd9f7 272 const SeekBar = videojs.getComponent('SeekBar') as any
2adfc7ea
C
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
293videojs.registerPlugin('peertube', PeerTubePlugin)
294export { PeerTubePlugin }