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