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