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