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