]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/assets/player/peertube-plugin.ts
Playlist support in watch page
[github/Chocobozzz/PeerTube.git] / client / src / assets / player / peertube-plugin.ts
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 }