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