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