]>
Commit | Line | Data |
---|---|---|
63c4db6d | 1 | import * as videojs from 'video.js' |
aa8b6df4 | 2 | import * as WebTorrent from 'webtorrent' |
b6827820 | 3 | import { VideoFile } from '../../../../shared/models/videos/video.model' |
aa8b6df4 | 4 | import { renderVideo } from './video-renderer' |
c6352f2c C |
5 | import './settings-menu-button' |
6 | import { PeertubePluginOptions, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | |
7 | import { getStoredMute, getStoredVolume, saveMuteInStore, saveVolumeInStore } from './utils' | |
be6a4802 | 8 | |
d402fb5b C |
9 | const webtorrent = new WebTorrent({ |
10 | tracker: { | |
11 | rtcConfig: { | |
12 | iceServers: [ | |
13 | { | |
14 | urls: 'stun:stun.stunprotocol.org' | |
15 | }, | |
16 | { | |
17 | urls: 'stun:stun.framasoft.org' | |
d402fb5b C |
18 | } |
19 | ] | |
20 | } | |
21 | }, | |
22 | dht: false | |
23 | }) | |
aa8b6df4 | 24 | |
a22bfc3e C |
25 | const Plugin: VideoJSComponentInterface = videojsUntyped.getPlugin('plugin') |
26 | class PeerTubePlugin extends Plugin { | |
c6352f2c C |
27 | private readonly playerElement: HTMLVideoElement |
28 | private readonly autoplay: boolean = false | |
29 | private readonly savePlayerSrcFunction: Function | |
a22bfc3e C |
30 | private player: any |
31 | private currentVideoFile: VideoFile | |
a22bfc3e C |
32 | private videoFiles: VideoFile[] |
33 | private torrent: WebTorrent.Torrent | |
8cac1b64 | 34 | private videoViewUrl: string |
3bcfff7f | 35 | private videoDuration: number |
8cac1b64 | 36 | private videoViewInterval |
3bcfff7f | 37 | private torrentInfoInterval |
a22bfc3e | 38 | |
339632b4 | 39 | constructor (player: videojs.Player, options: PeertubePluginOptions) { |
a22bfc3e C |
40 | super(player, options) |
41 | ||
481d3596 C |
42 | // Fix canplay event on google chrome by disabling default videojs autoplay |
43 | this.autoplay = this.player.options_.autoplay | |
44 | this.player.options_.autoplay = false | |
45 | ||
a22bfc3e | 46 | this.videoFiles = options.videoFiles |
8cac1b64 | 47 | this.videoViewUrl = options.videoViewUrl |
3bcfff7f | 48 | this.videoDuration = options.videoDuration |
a22bfc3e | 49 | |
bf5685f0 | 50 | this.savePlayerSrcFunction = this.player.src |
a22bfc3e C |
51 | // Hack to "simulate" src link in video.js >= 6 |
52 | // Without this, we can't play the video after pausing it | |
53 | // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633 | |
bf5685f0 | 54 | this.player.src = () => true |
a22bfc3e C |
55 | |
56 | this.playerElement = options.playerElement | |
57 | ||
58 | this.player.ready(() => { | |
c6352f2c C |
59 | const volume = getStoredVolume() |
60 | if (volume !== undefined) this.player.volume(volume) | |
61 | const muted = getStoredMute() | |
62 | if (muted !== undefined) this.player.muted(muted) | |
63 | ||
0dcf9a14 | 64 | this.initializePlayer() |
a22bfc3e | 65 | this.runTorrentInfoScheduler() |
3bcfff7f | 66 | this.runViewAdd() |
a22bfc3e | 67 | }) |
c6352f2c C |
68 | |
69 | this.player.on('volumechange', () => { | |
70 | saveVolumeInStore(this.player.volume()) | |
71 | saveMuteInStore(this.player.muted()) | |
72 | }) | |
a22bfc3e C |
73 | } |
74 | ||
75 | dispose () { | |
3bcfff7f C |
76 | clearInterval(this.videoViewInterval) |
77 | clearInterval(this.torrentInfoInterval) | |
78 | ||
a22bfc3e C |
79 | // Don't need to destroy renderer, video player will be destroyed |
80 | this.flushVideoFile(this.currentVideoFile, false) | |
aa8b6df4 C |
81 | } |
82 | ||
09700934 C |
83 | getCurrentResolutionId () { |
84 | return this.currentVideoFile ? this.currentVideoFile.resolution.id : -1 | |
aa8b6df4 C |
85 | } |
86 | ||
a22bfc3e | 87 | getCurrentResolutionLabel () { |
09700934 | 88 | return this.currentVideoFile ? this.currentVideoFile.resolution.label : '' |
aa8b6df4 C |
89 | } |
90 | ||
a22bfc3e | 91 | updateVideoFile (videoFile?: VideoFile, done?: () => void) { |
aa8b6df4 C |
92 | if (done === undefined) { |
93 | done = () => { /* empty */ } | |
94 | } | |
95 | ||
96 | // Pick the first one | |
97 | if (videoFile === undefined) { | |
a22bfc3e | 98 | videoFile = this.videoFiles[0] |
aa8b6df4 C |
99 | } |
100 | ||
101 | // Don't add the same video file once again | |
a22bfc3e | 102 | if (this.currentVideoFile !== undefined && this.currentVideoFile.magnetUri === videoFile.magnetUri) { |
aa8b6df4 C |
103 | return |
104 | } | |
105 | ||
c6352f2c | 106 | // Do not display error to user because we will have multiple fallback |
bf5685f0 | 107 | this.disableErrorDisplay() |
1198a08c | 108 | |
bf5685f0 | 109 | this.player.src = () => true |
c6352f2c | 110 | const oldPlaybackRate = this.player.playbackRate() |
bf5685f0 | 111 | |
a22bfc3e C |
112 | const previousVideoFile = this.currentVideoFile |
113 | this.currentVideoFile = videoFile | |
aa8b6df4 | 114 | |
c6352f2c C |
115 | this.addTorrent(this.currentVideoFile.magnetUri, previousVideoFile, () => { |
116 | this.player.playbackRate(oldPlaybackRate) | |
117 | return done() | |
118 | }) | |
a216c623 C |
119 | |
120 | this.trigger('videoFileUpdate') | |
121 | } | |
122 | ||
123 | addTorrent (magnetOrTorrentUrl: string, previousVideoFile: VideoFile, done: Function) { | |
124 | console.log('Adding ' + magnetOrTorrentUrl + '.') | |
125 | ||
126 | this.torrent = webtorrent.add(magnetOrTorrentUrl, torrent => { | |
127 | console.log('Added ' + magnetOrTorrentUrl + '.') | |
aa8b6df4 C |
128 | |
129 | this.flushVideoFile(previousVideoFile) | |
130 | ||
131 | const options = { autoplay: true, controls: true } | |
a22bfc3e | 132 | renderVideo(torrent.files[0], this.playerElement, options,(err, renderer) => { |
0dcf9a14 C |
133 | this.renderer = renderer |
134 | ||
c6352f2c | 135 | if (err) return this.fallbackToHttp(done) |
aa8b6df4 | 136 | |
3bcfff7f C |
137 | if (!this.player.paused()) { |
138 | const playPromise = this.player.play() | |
139 | if (playPromise !== undefined) return playPromise.then(done) | |
140 | ||
141 | return done() | |
142 | } | |
143 | ||
144 | return done() | |
aa8b6df4 C |
145 | }) |
146 | }) | |
147 | ||
a22bfc3e | 148 | this.torrent.on('error', err => this.handleError(err)) |
a216c623 | 149 | |
a22bfc3e | 150 | this.torrent.on('warning', (err: any) => { |
a96aed15 | 151 | // We don't support HTTP tracker but we don't care -> we use the web socket tracker |
531ab5b6 | 152 | if (err.message.indexOf('Unsupported tracker protocol') !== -1) return |
a216c623 | 153 | |
7dbdc3ba C |
154 | // Users don't care about issues with WebRTC, but developers do so log it in the console |
155 | if (err.message.indexOf('Ice connection failed') !== -1) { | |
156 | console.error(err) | |
157 | return | |
158 | } | |
a96aed15 | 159 | |
a216c623 C |
160 | // Magnet hash is not up to date with the torrent file, add directly the torrent file |
161 | if (err.message.indexOf('incorrect info hash') !== -1) { | |
162 | console.error('Incorrect info hash detected, falling back to torrent file.') | |
163 | return this.addTorrent(this.torrent['xs'], previousVideoFile, done) | |
164 | } | |
165 | ||
a22bfc3e | 166 | return this.handleError(err) |
a96aed15 | 167 | }) |
aa8b6df4 C |
168 | } |
169 | ||
09700934 | 170 | updateResolution (resolutionId: number) { |
aa8b6df4 | 171 | // Remember player state |
a22bfc3e C |
172 | const currentTime = this.player.currentTime() |
173 | const isPaused = this.player.paused() | |
aa8b6df4 | 174 | |
531ab5b6 C |
175 | // Remove poster to have black background |
176 | this.playerElement.poster = '' | |
177 | ||
aa8b6df4 | 178 | // Hide bigPlayButton |
8fa5653a | 179 | if (!isPaused) { |
a22bfc3e | 180 | this.player.bigPlayButton.hide() |
aa8b6df4 C |
181 | } |
182 | ||
09700934 | 183 | const newVideoFile = this.videoFiles.find(f => f.resolution.id === resolutionId) |
a22bfc3e C |
184 | this.updateVideoFile(newVideoFile, () => { |
185 | this.player.currentTime(currentTime) | |
186 | this.player.handleTechSeeked_() | |
aa8b6df4 C |
187 | }) |
188 | } | |
189 | ||
a22bfc3e | 190 | flushVideoFile (videoFile: VideoFile, destroyRenderer = true) { |
aa8b6df4 | 191 | if (videoFile !== undefined && webtorrent.get(videoFile.magnetUri)) { |
bf5685f0 C |
192 | if (destroyRenderer === true && this.renderer && this.renderer.destroy) this.renderer.destroy() |
193 | ||
aa8b6df4 | 194 | webtorrent.remove(videoFile.magnetUri) |
a22bfc3e | 195 | console.log('Removed ' + videoFile.magnetUri) |
aa8b6df4 C |
196 | } |
197 | } | |
198 | ||
3bcfff7f | 199 | setVideoFiles (files: VideoFile[], videoViewUrl: string, videoDuration: number) { |
8cac1b64 | 200 | this.videoViewUrl = videoViewUrl |
3bcfff7f | 201 | this.videoDuration = videoDuration |
a22bfc3e | 202 | this.videoFiles = files |
ed9f9f5f | 203 | |
8cac1b64 | 204 | // Re run view add for the new video |
3bcfff7f | 205 | this.runViewAdd() |
a22bfc3e | 206 | this.updateVideoFile(undefined, () => this.player.play()) |
ed9f9f5f C |
207 | } |
208 | ||
0dcf9a14 | 209 | private initializePlayer () { |
e993ecb3 C |
210 | this.initSmoothProgressBar() |
211 | ||
c6352f2c C |
212 | this.alterInactivity() |
213 | ||
481d3596 C |
214 | if (this.autoplay === true) { |
215 | this.updateVideoFile(undefined, () => this.player.play()) | |
aa8b6df4 | 216 | } else { |
c6352f2c C |
217 | // Proxify first play |
218 | const oldPlay = this.player.play.bind(this.player) | |
219 | this.player.play = () => { | |
220 | this.updateVideoFile(undefined, () => oldPlay) | |
221 | this.player.play = oldPlay | |
222 | } | |
aa8b6df4 | 223 | } |
a22bfc3e | 224 | } |
aa8b6df4 | 225 | |
a22bfc3e | 226 | private runTorrentInfoScheduler () { |
3bcfff7f | 227 | this.torrentInfoInterval = setInterval(() => { |
bf5685f0 C |
228 | // Not initialized yet |
229 | if (this.torrent === undefined) return | |
230 | ||
231 | // Http fallback | |
232 | if (this.torrent === null) return this.trigger('torrentInfo', false) | |
233 | ||
234 | return this.trigger('torrentInfo', { | |
235 | downloadSpeed: this.torrent.downloadSpeed, | |
236 | numPeers: this.torrent.numPeers, | |
237 | uploadSpeed: this.torrent.uploadSpeed | |
238 | }) | |
aa8b6df4 | 239 | }, 1000) |
a22bfc3e | 240 | } |
aa8b6df4 | 241 | |
8cac1b64 C |
242 | private runViewAdd () { |
243 | this.clearVideoViewInterval() | |
244 | ||
245 | // After 30 seconds (or 3/4 of the video), add a view to the video | |
246 | let minSecondsToView = 30 | |
247 | ||
3bcfff7f | 248 | if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4 |
8cac1b64 C |
249 | |
250 | let secondsViewed = 0 | |
251 | this.videoViewInterval = setInterval(() => { | |
252 | if (this.player && !this.player.paused()) { | |
253 | secondsViewed += 1 | |
254 | ||
255 | if (secondsViewed > minSecondsToView) { | |
256 | this.clearVideoViewInterval() | |
257 | ||
258 | this.addViewToVideo().catch(err => console.error(err)) | |
259 | } | |
260 | } | |
261 | }, 1000) | |
262 | } | |
263 | ||
264 | private clearVideoViewInterval () { | |
265 | if (this.videoViewInterval !== undefined) { | |
266 | clearInterval(this.videoViewInterval) | |
267 | this.videoViewInterval = undefined | |
268 | } | |
269 | } | |
270 | ||
271 | private addViewToVideo () { | |
272 | return fetch(this.videoViewUrl, { method: 'POST' }) | |
273 | } | |
274 | ||
c6352f2c | 275 | private fallbackToHttp (done: Function) { |
bf5685f0 C |
276 | this.flushVideoFile(this.currentVideoFile, true) |
277 | this.torrent = null | |
278 | ||
279 | // Enable error display now this is our last fallback | |
280 | this.player.one('error', () => this.enableErrorDisplay()) | |
281 | ||
282 | const httpUrl = this.currentVideoFile.fileUrl | |
283 | this.player.src = this.savePlayerSrcFunction | |
284 | this.player.src(httpUrl) | |
285 | this.player.play() | |
c6352f2c C |
286 | |
287 | return done() | |
bf5685f0 C |
288 | } |
289 | ||
a22bfc3e C |
290 | private handleError (err: Error | string) { |
291 | return this.player.trigger('customError', { err }) | |
aa8b6df4 | 292 | } |
bf5685f0 C |
293 | |
294 | private enableErrorDisplay () { | |
295 | this.player.addClass('vjs-error-display-enabled') | |
296 | } | |
297 | ||
298 | private disableErrorDisplay () { | |
299 | this.player.removeClass('vjs-error-display-enabled') | |
300 | } | |
e993ecb3 | 301 | |
c6352f2c C |
302 | private alterInactivity () { |
303 | let saveInactivityTimeout: number | |
304 | ||
305 | const disableInactivity = () => { | |
306 | saveInactivityTimeout = this.player.options_.inactivityTimeout | |
307 | this.player.options_.inactivityTimeout = 0 | |
308 | } | |
309 | const enableInactivity = () => { | |
310 | // this.player.options_.inactivityTimeout = saveInactivityTimeout | |
311 | } | |
312 | ||
313 | const settingsDialog = this.player.children_.find(c => c.name_ === 'SettingsDialog') | |
314 | ||
315 | this.player.controlBar.on('mouseenter', () => disableInactivity()) | |
316 | settingsDialog.on('mouseenter', () => disableInactivity()) | |
317 | this.player.controlBar.on('mouseleave', () => enableInactivity()) | |
318 | settingsDialog.on('mouseleave', () => enableInactivity()) | |
319 | } | |
320 | ||
e993ecb3 C |
321 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 |
322 | private initSmoothProgressBar () { | |
323 | const SeekBar = videojsUntyped.getComponent('SeekBar') | |
324 | SeekBar.prototype.getPercent = function getPercent () { | |
325 | // Allows for smooth scrubbing, when player can't keep up. | |
326 | // const time = (this.player_.scrubbing()) ? | |
327 | // this.player_.getCache().currentTime : | |
328 | // this.player_.currentTime() | |
329 | const time = this.player_.currentTime() | |
330 | const percent = time / this.player_.duration() | |
331 | return percent >= 1 ? 1 : percent | |
332 | } | |
333 | SeekBar.prototype.handleMouseMove = function handleMouseMove (event) { | |
334 | let newTime = this.calculateDistance(event) * this.player_.duration() | |
335 | if (newTime === this.player_.duration()) { | |
336 | newTime = newTime - 0.1 | |
337 | } | |
338 | this.player_.currentTime(newTime) | |
339 | this.update() | |
340 | } | |
341 | } | |
aa8b6df4 | 342 | } |
c6352f2c | 343 | |
a22bfc3e | 344 | videojsUntyped.registerPlugin('peertube', PeerTubePlugin) |
c6352f2c | 345 | export { PeerTubePlugin } |