]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/assets/player/peertube-plugin.ts
Add hls support on server
[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 autoplay: boolean = false
26 private readonly startTime: number = 0
27 private readonly videoViewUrl: string
28 private readonly videoDuration: number
29 private readonly CONSTANTS = {
30 USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
31 }
32
33 private player: any
34 private videoCaptions: VideoJSCaption[]
35 private defaultSubtitle: string
36
37 private videoViewInterval: any
38 private userWatchingVideoInterval: any
39 private qualityObservationTimer: any
40 private lastResolutionChange: ResolutionUpdateData
41
42 constructor (player: videojs.Player, options: PeerTubePluginOptions) {
43 super(player, options)
44
45 this.startTime = timeToInt(options.startTime)
46 this.videoViewUrl = options.videoViewUrl
47 this.videoDuration = options.videoDuration
48 this.videoCaptions = options.videoCaptions
49
50 if (this.autoplay === true) this.player.addClass('vjs-has-autoplay')
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 this.player.textTracks().on('change', () => {
85 const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => {
86 return t.kind === 'captions' && t.mode === 'showing'
87 })
88
89 if (!showing) {
90 saveLastSubtitle('off')
91 return
92 }
93
94 saveLastSubtitle(showing.language)
95 })
96
97 this.player.on('sourcechange', () => this.initCaptions())
98
99 this.player.duration(options.videoDuration)
100
101 this.initializePlayer()
102 this.runViewAdd()
103
104 if (options.userWatching) this.runUserWatchVideo(options.userWatching)
105 })
106 }
107
108 dispose () {
109 clearTimeout(this.qualityObservationTimer)
110
111 clearInterval(this.videoViewInterval)
112
113 if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
114 }
115
116 private initializePlayer () {
117 if (isMobile()) this.player.addClass('vjs-is-mobile')
118
119 this.initSmoothProgressBar()
120
121 this.initCaptions()
122
123 this.alterInactivity()
124 }
125
126 private runViewAdd () {
127 this.clearVideoViewInterval()
128
129 // After 30 seconds (or 3/4 of the video), add a view to the video
130 let minSecondsToView = 30
131
132 if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4
133
134 let secondsViewed = 0
135 this.videoViewInterval = setInterval(() => {
136 if (this.player && !this.player.paused()) {
137 secondsViewed += 1
138
139 if (secondsViewed > minSecondsToView) {
140 this.clearVideoViewInterval()
141
142 this.addViewToVideo().catch(err => console.error(err))
143 }
144 }
145 }, 1000)
146 }
147
148 private runUserWatchVideo (options: UserWatching) {
149 let lastCurrentTime = 0
150
151 this.userWatchingVideoInterval = setInterval(() => {
152 const currentTime = Math.floor(this.player.currentTime())
153
154 if (currentTime - lastCurrentTime >= 1) {
155 lastCurrentTime = currentTime
156
157 this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader)
158 .catch(err => console.error('Cannot notify user is watching.', err))
159 }
160 }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL)
161 }
162
163 private clearVideoViewInterval () {
164 if (this.videoViewInterval !== undefined) {
165 clearInterval(this.videoViewInterval)
166 this.videoViewInterval = undefined
167 }
168 }
169
170 private addViewToVideo () {
171 if (!this.videoViewUrl) return Promise.resolve(undefined)
172
173 return fetch(this.videoViewUrl, { method: 'POST' })
174 }
175
176 private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) {
177 const body = new URLSearchParams()
178 body.append('currentTime', currentTime.toString())
179
180 const headers = new Headers({ 'Authorization': authorizationHeader })
181
182 return fetch(url, { method: 'PUT', body, headers })
183 }
184
185 private handleResolutionChange (data: ResolutionUpdateData) {
186 this.lastResolutionChange = data
187
188 const qualityLevels = this.player.qualityLevels()
189
190 for (let i = 0; i < qualityLevels.length; i++) {
191 if (qualityLevels[i].height === data.resolutionId) {
192 data.id = qualityLevels[i].id
193 break
194 }
195 }
196
197 this.trigger('resolutionChange', data)
198 }
199
200 private alterInactivity () {
201 let saveInactivityTimeout: number
202
203 const disableInactivity = () => {
204 saveInactivityTimeout = this.player.options_.inactivityTimeout
205 this.player.options_.inactivityTimeout = 0
206 }
207 const enableInactivity = () => {
208 this.player.options_.inactivityTimeout = saveInactivityTimeout
209 }
210
211 const settingsDialog = this.player.children_.find((c: any) => c.name_ === 'SettingsDialog')
212
213 this.player.controlBar.on('mouseenter', () => disableInactivity())
214 settingsDialog.on('mouseenter', () => disableInactivity())
215 this.player.controlBar.on('mouseleave', () => enableInactivity())
216 settingsDialog.on('mouseleave', () => enableInactivity())
217 }
218
219 private initCaptions () {
220 for (const caption of this.videoCaptions) {
221 this.player.addRemoteTextTrack({
222 kind: 'captions',
223 label: caption.label,
224 language: caption.language,
225 id: caption.language,
226 src: caption.src,
227 default: this.defaultSubtitle === caption.language
228 }, false)
229 }
230
231 this.player.trigger('captionsChanged')
232 }
233
234 // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
235 private initSmoothProgressBar () {
236 const SeekBar = videojsUntyped.getComponent('SeekBar')
237 SeekBar.prototype.getPercent = function getPercent () {
238 // Allows for smooth scrubbing, when player can't keep up.
239 // const time = (this.player_.scrubbing()) ?
240 // this.player_.getCache().currentTime :
241 // this.player_.currentTime()
242 const time = this.player_.currentTime()
243 const percent = time / this.player_.duration()
244 return percent >= 1 ? 1 : percent
245 }
246 SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) {
247 let newTime = this.calculateDistance(event) * this.player_.duration()
248 if (newTime === this.player_.duration()) {
249 newTime = newTime - 0.1
250 }
251 this.player_.currentTime(newTime)
252 this.update()
253 }
254 }
255 }
256
257 videojs.registerPlugin('peertube', PeerTubePlugin)
258 export { PeerTubePlugin }