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