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