]>
Commit | Line | Data |
---|---|---|
9af2acce | 1 | import debug from 'debug' |
15a7eafb | 2 | import videojs from 'video.js' |
42b40636 | 3 | import { logger } from '@root-helpers/logger' |
57d65032 | 4 | import { isMobile } from '@root-helpers/web-browser' |
15a7eafb | 5 | import { timeToInt } from '@shared/core-utils' |
384ba8b7 | 6 | import { VideoView, VideoViewEvent } from '@shared/models/videos' |
2adfc7ea C |
7 | import { |
8 | getStoredLastSubtitle, | |
9 | getStoredMute, | |
10 | getStoredVolume, | |
11 | saveLastSubtitle, | |
12 | saveMuteInStore, | |
58b9ce30 | 13 | saveVideoWatchHistory, |
2adfc7ea | 14 | saveVolumeInStore |
57d65032 | 15 | } from '../../peertube-player-local-storage' |
384ba8b7 | 16 | import { PeerTubePluginOptions, VideoJSCaption } from '../../types' |
57d65032 | 17 | import { SettingsButton } from '../settings/settings-menu-button' |
f1a0555a | 18 | |
42b40636 | 19 | const debugLogger = debug('peertube:player:peertube') |
2adfc7ea | 20 | |
f5fcd9f7 C |
21 | const Plugin = videojs.getPlugin('plugin') |
22 | ||
2adfc7ea | 23 | class PeerTubePlugin extends Plugin { |
2adfc7ea | 24 | private readonly videoViewUrl: string |
384ba8b7 C |
25 | private readonly authorizationHeader: string |
26 | ||
27 | private readonly videoUUID: string | |
28 | private readonly startTime: number | |
29 | ||
2adfc7ea | 30 | private readonly CONSTANTS = { |
384ba8b7 | 31 | USER_VIEW_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video |
2adfc7ea C |
32 | } |
33 | ||
2adfc7ea C |
34 | private videoCaptions: VideoJSCaption[] |
35 | private defaultSubtitle: string | |
36 | ||
37 | private videoViewInterval: any | |
10f26f42 | 38 | |
d1f21ebb C |
39 | private menuOpened = false |
40 | private mouseInControlBar = false | |
dc9ff312 C |
41 | private mouseInSettings = false |
42 | private readonly initialInactivityTimeout: number | |
d1f21ebb | 43 | |
7e37e111 | 44 | constructor (player: videojs.Player, options?: PeerTubePluginOptions) { |
f5fcd9f7 | 45 | super(player) |
2adfc7ea | 46 | |
2adfc7ea | 47 | this.videoViewUrl = options.videoViewUrl |
384ba8b7 C |
48 | this.authorizationHeader = options.authorizationHeader |
49 | this.videoUUID = options.videoUUID | |
50 | this.startTime = timeToInt(options.startTime) | |
51 | ||
2adfc7ea | 52 | this.videoCaptions = options.videoCaptions |
dc9ff312 | 53 | this.initialInactivityTimeout = this.player.options_.inactivityTimeout |
d1f21ebb | 54 | |
72efdda5 | 55 | if (options.autoplay) this.player.addClass('vjs-has-autoplay') |
6ec0b75b C |
56 | |
57 | this.player.on('autoplay-failure', () => { | |
58 | this.player.removeClass('vjs-has-autoplay') | |
59 | }) | |
2adfc7ea C |
60 | |
61 | this.player.ready(() => { | |
62 | const playerOptions = this.player.options_ | |
63 | ||
64 | const volume = getStoredVolume() | |
65 | if (volume !== undefined) this.player.volume(volume) | |
66 | ||
67 | const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() | |
68 | if (muted !== undefined) this.player.muted(muted) | |
69 | ||
70 | this.defaultSubtitle = options.subtitle || getStoredLastSubtitle() | |
71 | ||
72 | this.player.on('volumechange', () => { | |
73 | saveVolumeInStore(this.player.volume()) | |
74 | saveMuteInStore(this.player.muted()) | |
75 | }) | |
76 | ||
f0a39880 C |
77 | if (options.stopTime) { |
78 | const stopTime = timeToInt(options.stopTime) | |
e2f01c47 | 79 | const self = this |
f0a39880 | 80 | |
e2f01c47 C |
81 | this.player.on('timeupdate', function onTimeUpdate () { |
82 | if (self.player.currentTime() > stopTime) { | |
83 | self.player.pause() | |
84 | self.player.trigger('stopped') | |
85 | ||
86 | self.player.off('timeupdate', onTimeUpdate) | |
87 | } | |
f0a39880 C |
88 | }) |
89 | } | |
90 | ||
e367da94 | 91 | this.player.textTracks().addEventListener('change', () => { |
f5fcd9f7 | 92 | const showing = this.player.textTracks().tracks_.find(t => { |
2adfc7ea C |
93 | return t.kind === 'captions' && t.mode === 'showing' |
94 | }) | |
95 | ||
96 | if (!showing) { | |
97 | saveLastSubtitle('off') | |
98 | return | |
99 | } | |
100 | ||
101 | saveLastSubtitle(showing.language) | |
102 | }) | |
103 | ||
104 | this.player.on('sourcechange', () => this.initCaptions()) | |
105 | ||
106 | this.player.duration(options.videoDuration) | |
107 | ||
108 | this.initializePlayer() | |
384ba8b7 | 109 | this.runUserViewing() |
2adfc7ea C |
110 | }) |
111 | } | |
112 | ||
113 | dispose () { | |
f0a39880 | 114 | if (this.videoViewInterval) clearInterval(this.videoViewInterval) |
2adfc7ea C |
115 | } |
116 | ||
dc9ff312 C |
117 | onMenuOpened () { |
118 | this.menuOpened = true | |
d1f21ebb C |
119 | this.alterInactivity() |
120 | } | |
121 | ||
122 | onMenuClosed () { | |
dc9ff312 | 123 | this.menuOpened = false |
d1f21ebb C |
124 | this.alterInactivity() |
125 | } | |
126 | ||
c4207f97 | 127 | displayFatalError () { |
f2a16d93 | 128 | this.player.loadingSpinner.hide() |
129 | ||
130 | const buildModal = (error: MediaError) => { | |
131 | const localize = this.player.localize.bind(this.player) | |
132 | ||
133 | const wrapper = document.createElement('div') | |
134 | const header = document.createElement('h1') | |
135 | header.innerText = localize('Failed to play video') | |
136 | wrapper.appendChild(header) | |
137 | const desc = document.createElement('div') | |
138 | desc.innerText = localize('The video failed to play due to technical issues.') | |
139 | wrapper.appendChild(desc) | |
140 | const details = document.createElement('p') | |
141 | details.classList.add('error-details') | |
142 | details.innerText = error.message | |
143 | wrapper.appendChild(details) | |
144 | ||
145 | return wrapper | |
146 | } | |
147 | ||
148 | const modal = this.player.createModal(buildModal(this.player.error()), { | |
149 | temporary: false, | |
150 | uncloseable: true | |
151 | }) | |
152 | modal.addClass('vjs-custom-error-display') | |
153 | ||
c4207f97 C |
154 | this.player.addClass('vjs-error-display-enabled') |
155 | } | |
156 | ||
157 | hideFatalError () { | |
158 | this.player.removeClass('vjs-error-display-enabled') | |
159 | } | |
160 | ||
2adfc7ea C |
161 | private initializePlayer () { |
162 | if (isMobile()) this.player.addClass('vjs-is-mobile') | |
163 | ||
164 | this.initSmoothProgressBar() | |
165 | ||
166 | this.initCaptions() | |
167 | ||
d1f21ebb | 168 | this.listenControlBarMouse() |
07d6044e C |
169 | |
170 | this.listenFullScreenChange() | |
2adfc7ea C |
171 | } |
172 | ||
fd3c2e87 C |
173 | // --------------------------------------------------------------------------- |
174 | ||
384ba8b7 C |
175 | private runUserViewing () { |
176 | let lastCurrentTime = this.startTime | |
177 | let lastViewEvent: VideoViewEvent | |
2adfc7ea | 178 | |
384ba8b7 C |
179 | this.player.one('play', () => { |
180 | this.notifyUserIsWatching(this.startTime, lastViewEvent) | |
181 | }) | |
2adfc7ea | 182 | |
384ba8b7 C |
183 | this.player.on('seeked', () => { |
184 | // Don't take into account small seek events | |
185 | if (Math.abs(this.player.currentTime() - lastCurrentTime) < 3) return | |
2adfc7ea | 186 | |
384ba8b7 C |
187 | lastViewEvent = 'seek' |
188 | }) | |
2adfc7ea | 189 | |
384ba8b7 | 190 | this.player.one('ended', () => { |
1222a602 | 191 | const currentTime = Math.round(this.player.duration()) |
384ba8b7 | 192 | lastCurrentTime = currentTime |
2adfc7ea | 193 | |
384ba8b7 | 194 | this.notifyUserIsWatching(currentTime, lastViewEvent) |
2adfc7ea | 195 | |
384ba8b7 C |
196 | lastViewEvent = undefined |
197 | }) | |
198 | ||
199 | this.videoViewInterval = setInterval(() => { | |
1222a602 | 200 | const currentTime = Math.round(this.player.currentTime()) |
2adfc7ea | 201 | |
384ba8b7 C |
202 | // No need to update |
203 | if (currentTime === lastCurrentTime) return | |
2adfc7ea | 204 | |
384ba8b7 | 205 | lastCurrentTime = currentTime |
2adfc7ea | 206 | |
384ba8b7 | 207 | this.notifyUserIsWatching(currentTime, lastViewEvent) |
42b40636 | 208 | .catch(err => logger.error('Cannot notify user is watching.', err)) |
384ba8b7 C |
209 | |
210 | lastViewEvent = undefined | |
211 | ||
212 | // Server won't save history, so save the video position in local storage | |
213 | if (!this.authorizationHeader) { | |
214 | saveVideoWatchHistory(this.videoUUID, currentTime) | |
215 | } | |
216 | }, this.CONSTANTS.USER_VIEW_VIDEO_INTERVAL) | |
2adfc7ea C |
217 | } |
218 | ||
384ba8b7 | 219 | private notifyUserIsWatching (currentTime: number, viewEvent: VideoViewEvent) { |
2adfc7ea C |
220 | if (!this.videoViewUrl) return Promise.resolve(undefined) |
221 | ||
384ba8b7 C |
222 | const body: VideoView = { |
223 | currentTime, | |
224 | viewEvent | |
225 | } | |
2adfc7ea | 226 | |
384ba8b7 C |
227 | const headers = new Headers({ |
228 | 'Content-type': 'application/json; charset=UTF-8' | |
229 | }) | |
2adfc7ea | 230 | |
384ba8b7 | 231 | if (this.authorizationHeader) headers.set('Authorization', this.authorizationHeader) |
2adfc7ea | 232 | |
384ba8b7 | 233 | return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers }) |
2adfc7ea C |
234 | } |
235 | ||
fd3c2e87 C |
236 | // --------------------------------------------------------------------------- |
237 | ||
07d6044e C |
238 | private listenFullScreenChange () { |
239 | this.player.on('fullscreenchange', () => { | |
240 | if (this.player.isFullscreen()) this.player.focus() | |
241 | }) | |
242 | } | |
243 | ||
d1f21ebb | 244 | private listenControlBarMouse () { |
dc9ff312 C |
245 | const controlBar = this.player.controlBar |
246 | const settingsButton: SettingsButton = (controlBar as any).settingsButton | |
247 | ||
248 | controlBar.on('mouseenter', () => { | |
d1f21ebb C |
249 | this.mouseInControlBar = true |
250 | this.alterInactivity() | |
251 | }) | |
2adfc7ea | 252 | |
dc9ff312 | 253 | controlBar.on('mouseleave', () => { |
d1f21ebb C |
254 | this.mouseInControlBar = false |
255 | this.alterInactivity() | |
256 | }) | |
dc9ff312 C |
257 | |
258 | settingsButton.dialog.on('mouseenter', () => { | |
259 | this.mouseInSettings = true | |
260 | this.alterInactivity() | |
261 | }) | |
262 | ||
263 | settingsButton.dialog.on('mouseleave', () => { | |
264 | this.mouseInSettings = false | |
265 | this.alterInactivity() | |
266 | }) | |
d1f21ebb | 267 | } |
2adfc7ea | 268 | |
d1f21ebb | 269 | private alterInactivity () { |
f1a0555a | 270 | if (this.menuOpened || this.mouseInSettings || this.mouseInControlBar) { |
dc9ff312 | 271 | this.setInactivityTimeout(0) |
d1f21ebb C |
272 | return |
273 | } | |
2adfc7ea | 274 | |
dc9ff312 C |
275 | this.setInactivityTimeout(this.initialInactivityTimeout) |
276 | this.player.reportUserActivity(true) | |
277 | } | |
278 | ||
279 | private setInactivityTimeout (timeout: number) { | |
280 | (this.player as any).cache_.inactivityTimeout = timeout | |
281 | this.player.options_.inactivityTimeout = timeout | |
f1a0555a | 282 | |
42b40636 | 283 | debugLogger('Set player inactivity to ' + timeout) |
35f0a5e6 C |
284 | } |
285 | ||
2adfc7ea C |
286 | private initCaptions () { |
287 | for (const caption of this.videoCaptions) { | |
288 | this.player.addRemoteTextTrack({ | |
289 | kind: 'captions', | |
290 | label: caption.label, | |
291 | language: caption.language, | |
292 | id: caption.language, | |
293 | src: caption.src, | |
294 | default: this.defaultSubtitle === caption.language | |
295 | }, false) | |
296 | } | |
297 | ||
298 | this.player.trigger('captionsChanged') | |
299 | } | |
300 | ||
301 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 | |
302 | private initSmoothProgressBar () { | |
f5fcd9f7 | 303 | const SeekBar = videojs.getComponent('SeekBar') as any |
2adfc7ea C |
304 | SeekBar.prototype.getPercent = function getPercent () { |
305 | // Allows for smooth scrubbing, when player can't keep up. | |
306 | // const time = (this.player_.scrubbing()) ? | |
307 | // this.player_.getCache().currentTime : | |
308 | // this.player_.currentTime() | |
309 | const time = this.player_.currentTime() | |
310 | const percent = time / this.player_.duration() | |
311 | return percent >= 1 ? 1 : percent | |
312 | } | |
313 | SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) { | |
314 | let newTime = this.calculateDistance(event) * this.player_.duration() | |
315 | if (newTime === this.player_.duration()) { | |
316 | newTime = newTime - 0.1 | |
317 | } | |
318 | this.player_.currentTime(newTime) | |
319 | this.update() | |
320 | } | |
321 | } | |
322 | } | |
323 | ||
324 | videojs.registerPlugin('peertube', PeerTubePlugin) | |
325 | export { PeerTubePlugin } |