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