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