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