Fix player lint
[oweals/peertube.git] / client / src / standalone / videos / embed.ts
1 import './embed.scss'
2
3 import 'core-js/es6/symbol'
4 import 'core-js/es6/object'
5 import 'core-js/es6/function'
6 import 'core-js/es6/parse-int'
7 import 'core-js/es6/parse-float'
8 import 'core-js/es6/number'
9 import 'core-js/es6/math'
10 import 'core-js/es6/string'
11 import 'core-js/es6/date'
12 import 'core-js/es6/array'
13 import 'core-js/es6/regexp'
14 import 'core-js/es6/map'
15 import 'core-js/es6/weak-map'
16 import 'core-js/es6/set'
17 // For google bot that uses Chrome 41 and does not understand fetch
18 import 'whatwg-fetch'
19
20 import * as vjs from 'video.js'
21 import * as Channel from 'jschannel'
22
23 import { VideoDetails } from '../../../../shared'
24 import { addContextMenu, getVideojsOptions, loadLocale } from '../../assets/player/peertube-player'
25 import { PeerTubeResolution } from '../player/definitions'
26
27 /**
28  * Embed API exposes control of the embed player to the outside world via
29  * JSChannels and window.postMessage
30  */
31 class PeerTubeEmbedApi {
32   private channel: Channel.MessagingChannel
33   private isReady = false
34   private resolutions: PeerTubeResolution[] = null
35
36   constructor (private embed: PeerTubeEmbed) {
37   }
38
39   initialize () {
40     this.constructChannel()
41     this.setupStateTracking()
42
43     // We're ready!
44
45     this.notifyReady()
46   }
47
48   private get element () {
49     return this.embed.videoElement
50   }
51
52   private constructChannel () {
53     let channel = Channel.build({ window: window.parent, origin: '*', scope: this.embed.scope })
54
55     channel.bind('play', (txn, params) => this.embed.player.play())
56     channel.bind('pause', (txn, params) => this.embed.player.pause())
57     channel.bind('seek', (txn, time) => this.embed.player.currentTime(time))
58     channel.bind('setVolume', (txn, value) => this.embed.player.volume(value))
59     channel.bind('getVolume', (txn, value) => this.embed.player.volume())
60     channel.bind('isReady', (txn, params) => this.isReady)
61     channel.bind('setResolution', (txn, resolutionId) => this.setResolution(resolutionId))
62     channel.bind('getResolutions', (txn, params) => this.resolutions)
63     channel.bind('setPlaybackRate', (txn, playbackRate) => this.embed.player.playbackRate(playbackRate))
64     channel.bind('getPlaybackRate', (txn, params) => this.embed.player.playbackRate())
65     channel.bind('getPlaybackRates', (txn, params) => this.embed.playerOptions.playbackRates)
66
67     this.channel = channel
68   }
69
70   private setResolution (resolutionId: number) {
71     if (resolutionId === -1 && this.embed.player.peertube().isAutoResolutionForbidden()) return
72
73     // Auto resolution
74     if (resolutionId === -1) {
75       this.embed.player.peertube().enableAutoResolution()
76       return
77     }
78
79     this.embed.player.peertube().disableAutoResolution()
80     this.embed.player.peertube().updateResolution(resolutionId)
81   }
82
83   /**
84    * Let the host know that we're ready to go!
85    */
86   private notifyReady () {
87     this.isReady = true
88     this.channel.notify({ method: 'ready', params: true })
89   }
90
91   private setupStateTracking () {
92     let currentState: 'playing' | 'paused' | 'unstarted' = 'unstarted'
93
94     setInterval(() => {
95       let position = this.element.currentTime
96       let volume = this.element.volume
97
98       this.channel.notify({
99         method: 'playbackStatusUpdate',
100         params: {
101           position,
102           volume,
103           playbackState: currentState
104         }
105       })
106     }, 500)
107
108     this.element.addEventListener('play', ev => {
109       currentState = 'playing'
110       this.channel.notify({ method: 'playbackStatusChange', params: 'playing' })
111     })
112
113     this.element.addEventListener('pause', ev => {
114       currentState = 'paused'
115       this.channel.notify({ method: 'playbackStatusChange', params: 'paused' })
116     })
117
118     // PeerTube specific capabilities
119
120     this.embed.player.peertube().on('autoResolutionUpdate', () => this.loadResolutions())
121     this.embed.player.peertube().on('videoFileUpdate', () => this.loadResolutions())
122   }
123
124   private loadResolutions () {
125     let resolutions = []
126     let currentResolutionId = this.embed.player.peertube().getCurrentResolutionId()
127
128     for (const videoFile of this.embed.player.peertube().videoFiles) {
129       let label = videoFile.resolution.label
130       if (videoFile.fps && videoFile.fps >= 50) {
131         label += videoFile.fps
132       }
133
134       resolutions.push({
135         id: videoFile.resolution.id,
136         label,
137         src: videoFile.magnetUri,
138         active: videoFile.resolution.id === currentResolutionId
139       })
140     }
141
142     this.resolutions = resolutions
143     this.channel.notify({
144       method: 'resolutionUpdate',
145       params: this.resolutions
146     })
147   }
148 }
149
150 class PeerTubeEmbed {
151   videoElement: HTMLVideoElement
152   player: any
153   playerOptions: any
154   api: PeerTubeEmbedApi = null
155   autoplay = false
156   controls = true
157   muted = false
158   loop = false
159   enableApi = false
160   startTime = 0
161   scope = 'peertube'
162
163   static async main () {
164     const videoContainerId = 'video-container'
165     const embed = new PeerTubeEmbed(videoContainerId)
166     await embed.init()
167   }
168
169   constructor (private videoContainerId: string) {
170     this.videoElement = document.getElementById(videoContainerId) as HTMLVideoElement
171   }
172
173   getVideoUrl (id: string) {
174     return window.location.origin + '/api/v1/videos/' + id
175   }
176
177   loadVideoInfo (videoId: string): Promise<Response> {
178     return fetch(this.getVideoUrl(videoId))
179   }
180
181   removeElement (element: HTMLElement) {
182     element.parentElement.removeChild(element)
183   }
184
185   displayError (videoElement: HTMLVideoElement, text: string) {
186     // Remove video element
187     this.removeElement(videoElement)
188
189     document.title = 'Sorry - ' + text
190
191     const errorBlock = document.getElementById('error-block')
192     errorBlock.style.display = 'flex'
193
194     const errorText = document.getElementById('error-content')
195     errorText.innerHTML = text
196   }
197
198   videoNotFound (videoElement: HTMLVideoElement) {
199     const text = 'This video does not exist.'
200     this.displayError(videoElement, text)
201   }
202
203   videoFetchError (videoElement: HTMLVideoElement) {
204     const text = 'We cannot fetch the video. Please try again later.'
205     this.displayError(videoElement, text)
206   }
207
208   getParamToggle (params: URLSearchParams, name: string, defaultValue: boolean) {
209     return params.has(name) ? (params.get(name) === '1' || params.get(name) === 'true') : defaultValue
210   }
211
212   getParamString (params: URLSearchParams, name: string, defaultValue: string) {
213     return params.has(name) ? params.get(name) : defaultValue
214   }
215
216   async init () {
217     try {
218       await this.initCore()
219     } catch (e) {
220       console.error(e)
221     }
222   }
223
224   private initializeApi () {
225     if (!this.enableApi) return
226
227     this.api = new PeerTubeEmbedApi(this)
228     this.api.initialize()
229   }
230
231   private loadParams () {
232     try {
233       let params = new URL(window.location.toString()).searchParams
234
235       this.autoplay = this.getParamToggle(params, 'autoplay', this.autoplay)
236       this.controls = this.getParamToggle(params, 'controls', this.controls)
237       this.muted = this.getParamToggle(params, 'muted', this.muted)
238       this.loop = this.getParamToggle(params, 'loop', this.loop)
239       this.enableApi = this.getParamToggle(params, 'api', this.enableApi)
240       this.scope = this.getParamString(params, 'scope', this.scope)
241
242       const startTimeParamString = params.get('start')
243       const startTimeParamNumber = parseInt(startTimeParamString, 10)
244
245       if (isNaN(startTimeParamNumber) === false) this.startTime = startTimeParamNumber
246     } catch (err) {
247       console.error('Cannot get params from URL.', err)
248     }
249   }
250
251   private async initCore () {
252     const urlParts = window.location.href.split('/')
253     const lastPart = urlParts[ urlParts.length - 1 ]
254     const videoId = lastPart.indexOf('?') === -1 ? lastPart : lastPart.split('?')[ 0 ]
255
256     await loadLocale(window.location.origin, vjs, navigator.language)
257     let response = await this.loadVideoInfo(videoId)
258
259     if (!response.ok) {
260       if (response.status === 404) return this.videoNotFound(this.videoElement)
261
262       return this.videoFetchError(this.videoElement)
263     }
264
265     const videoInfo: VideoDetails = await response.json()
266
267     this.loadParams()
268
269     const videojsOptions = getVideojsOptions({
270       autoplay: this.autoplay,
271       controls: this.controls,
272       muted: this.muted,
273       loop: this.loop,
274       startTime: this.startTime,
275
276       inactivityTimeout: 1500,
277       videoViewUrl: this.getVideoUrl(videoId) + '/views',
278       playerElement: this.videoElement,
279       videoFiles: videoInfo.files,
280       videoDuration: videoInfo.duration,
281       enableHotkeys: true,
282       peertubeLink: true,
283       poster: window.location.origin + videoInfo.previewPath,
284       theaterMode: false
285     })
286
287     this.playerOptions = videojsOptions
288     this.player = vjs(this.videoContainerId, videojsOptions, () => {
289
290       window[ 'videojsPlayer' ] = this.player
291
292       if (this.controls) {
293         this.player.dock({
294           title: videoInfo.name,
295           description: this.player.localize('Uses P2P, others may know your IP is downloading this video.')
296         })
297       }
298
299       addContextMenu(this.player, window.location.origin + videoInfo.embedPath)
300       this.initializeApi()
301     })
302   }
303 }
304
305 PeerTubeEmbed.main()
306   .catch(err => console.error('Cannot init embed.', err))