]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/standalone/videos/embed.ts
dca23ca7ea21777d293d4d69ca6e376b6801641f
[github/Chocobozzz/PeerTube.git] / client / src / standalone / videos / embed.ts
1 import './embed.scss'
2
3 import {
4 peertubeTranslate,
5 ResultList,
6 ServerConfig,
7 VideoDetails
8 } from '../../../../shared'
9 import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model'
10 import {
11 P2PMediaLoaderOptions,
12 PeertubePlayerManagerOptions,
13 PlayerMode
14 } from '../../assets/player/peertube-player-manager'
15 import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
16 import { PeerTubeEmbedApi } from './embed-api'
17 import { TranslationsManager } from '../../assets/player/translations-manager'
18 import videojs from 'video.js'
19 import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
20 import { AuthUser } from '@app/core/auth/auth-user.model'
21
22 type Translations = { [ id: string ]: string }
23
24 export class PeerTubeEmbed {
25 videoElement: HTMLVideoElement
26 player: videojs.Player
27 api: PeerTubeEmbedApi = null
28 autoplay: boolean
29 controls: boolean
30 muted: boolean
31 loop: boolean
32 subtitle: string
33 enableApi = false
34 startTime: number | string = 0
35 stopTime: number | string
36
37 title: boolean
38 warningTitle: boolean
39 peertubeLink: boolean
40 bigPlayBackgroundColor: string
41 foregroundColor: string
42
43 mode: PlayerMode
44 scope = 'peertube'
45
46 user: AuthUser
47 headers = new Headers()
48
49 static async main () {
50 const videoContainerId = 'video-container'
51 const embed = new PeerTubeEmbed(videoContainerId)
52 await embed.init()
53 }
54
55 constructor (private videoContainerId: string) {
56 this.videoElement = document.getElementById(videoContainerId) as HTMLVideoElement
57 }
58
59 getVideoUrl (id: string) {
60 return window.location.origin + '/api/v1/videos/' + id
61 }
62
63 loadVideoInfo (videoId: string): Promise<Response> {
64 return fetch(this.getVideoUrl(videoId), { headers: this.headers })
65 }
66
67 loadVideoCaptions (videoId: string): Promise<Response> {
68 return fetch(this.getVideoUrl(videoId) + '/captions', { headers: this.headers })
69 }
70
71 loadConfig (): Promise<Response> {
72 return fetch('/api/v1/config')
73 }
74
75 removeElement (element: HTMLElement) {
76 element.parentElement.removeChild(element)
77 }
78
79 displayError (text: string, translations?: Translations) {
80 // Remove video element
81 if (this.videoElement) this.removeElement(this.videoElement)
82
83 const translatedText = peertubeTranslate(text, translations)
84 const translatedSorry = peertubeTranslate('Sorry', translations)
85
86 document.title = translatedSorry + ' - ' + translatedText
87
88 const errorBlock = document.getElementById('error-block')
89 errorBlock.style.display = 'flex'
90
91 const errorTitle = document.getElementById('error-title')
92 errorTitle.innerHTML = peertubeTranslate('Sorry', translations)
93
94 const errorText = document.getElementById('error-content')
95 errorText.innerHTML = translatedText
96 }
97
98 videoNotFound (translations?: Translations) {
99 const text = 'This video does not exist.'
100 this.displayError(text, translations)
101 }
102
103 videoFetchError (translations?: Translations) {
104 const text = 'We cannot fetch the video. Please try again later.'
105 this.displayError(text, translations)
106 }
107
108 getParamToggle (params: URLSearchParams, name: string, defaultValue?: boolean) {
109 return params.has(name) ? (params.get(name) === '1' || params.get(name) === 'true') : defaultValue
110 }
111
112 getParamString (params: URLSearchParams, name: string, defaultValue?: string) {
113 return params.has(name) ? params.get(name) : defaultValue
114 }
115
116 async init () {
117 try {
118 this.user = AuthUser.load()
119 await this.initCore()
120 } catch (e) {
121 console.error(e)
122 }
123 }
124
125 private initializeApi () {
126 if (!this.enableApi) return
127
128 this.api = new PeerTubeEmbedApi(this)
129 this.api.initialize()
130 }
131
132 private loadParams (video: VideoDetails) {
133 try {
134 const params = new URL(window.location.toString()).searchParams
135
136 this.autoplay = this.getParamToggle(params, 'autoplay', false)
137 this.controls = this.getParamToggle(params, 'controls', true)
138 this.muted = this.getParamToggle(params, 'muted', undefined)
139 this.loop = this.getParamToggle(params, 'loop', false)
140 this.title = this.getParamToggle(params, 'title', true)
141 this.enableApi = this.getParamToggle(params, 'api', this.enableApi)
142 this.warningTitle = this.getParamToggle(params, 'warningTitle', true)
143 this.peertubeLink = this.getParamToggle(params, 'peertubeLink', true)
144
145 this.scope = this.getParamString(params, 'scope', this.scope)
146 this.subtitle = this.getParamString(params, 'subtitle')
147 this.startTime = this.getParamString(params, 'start')
148 this.stopTime = this.getParamString(params, 'stop')
149
150 this.bigPlayBackgroundColor = this.getParamString(params, 'bigPlayBackgroundColor')
151 this.foregroundColor = this.getParamString(params, 'foregroundColor')
152
153 const modeParam = this.getParamString(params, 'mode')
154
155 if (modeParam) {
156 if (modeParam === 'p2p-media-loader') this.mode = 'p2p-media-loader'
157 else this.mode = 'webtorrent'
158 } else {
159 if (Array.isArray(video.streamingPlaylists) && video.streamingPlaylists.length !== 0) this.mode = 'p2p-media-loader'
160 else this.mode = 'webtorrent'
161 }
162 } catch (err) {
163 console.error('Cannot get params from URL.', err)
164 }
165 }
166
167 private async initCore () {
168 const urlParts = window.location.pathname.split('/')
169 const videoId = urlParts[ urlParts.length - 1 ]
170
171 if (this.user) {
172 this.headers.set('Authorization', `${this.user.getTokenType()} ${this.user.getAccessToken()}`)
173 }
174
175 const videoPromise = this.loadVideoInfo(videoId)
176 const captionsPromise = this.loadVideoCaptions(videoId)
177 const configPromise = this.loadConfig()
178
179 const translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language)
180 const videoResponse = await videoPromise
181
182 if (!videoResponse.ok) {
183 const serverTranslations = await translationsPromise
184
185 if (videoResponse.status === 404) return this.videoNotFound(serverTranslations)
186
187 return this.videoFetchError(serverTranslations)
188 }
189
190 const videoInfo: VideoDetails = await videoResponse.json()
191 this.loadPlaceholder(videoInfo)
192
193 const PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager')
194
195 const promises = [ translationsPromise, captionsPromise, configPromise, PeertubePlayerManagerModulePromise ]
196 const [ serverTranslations, captionsResponse, configResponse, PeertubePlayerManagerModule ] = await Promise.all(promises)
197
198 const PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager
199 const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse)
200
201 this.loadParams(videoInfo)
202
203 const options: PeertubePlayerManagerOptions = {
204 common: {
205 autoplay: this.autoplay,
206 controls: this.controls,
207 muted: this.muted,
208 loop: this.loop,
209 captions: videoCaptions.length !== 0,
210 startTime: this.startTime,
211 stopTime: this.stopTime,
212 subtitle: this.subtitle,
213
214 videoCaptions,
215 inactivityTimeout: 2500,
216 videoViewUrl: this.getVideoUrl(videoId) + '/views',
217
218 playerElement: this.videoElement,
219 onPlayerElementChange: (element: HTMLVideoElement) => this.videoElement = element,
220
221 videoDuration: videoInfo.duration,
222 enableHotkeys: true,
223 peertubeLink: this.peertubeLink,
224 poster: window.location.origin + videoInfo.previewPath,
225 theaterButton: false,
226
227 serverUrl: window.location.origin,
228 language: navigator.language,
229 embedUrl: window.location.origin + videoInfo.embedPath
230 },
231
232 webtorrent: {
233 videoFiles: videoInfo.files
234 }
235 }
236
237 if (this.mode === 'p2p-media-loader') {
238 const hlsPlaylist = videoInfo.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
239
240 Object.assign(options, {
241 p2pMediaLoader: {
242 playlistUrl: hlsPlaylist.playlistUrl,
243 segmentsSha256Url: hlsPlaylist.segmentsSha256Url,
244 redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl),
245 trackerAnnounce: videoInfo.trackerUrls,
246 videoFiles: hlsPlaylist.files
247 } as P2PMediaLoaderOptions
248 })
249 }
250
251 this.player = await PeertubePlayerManager.initialize(this.mode, options, (player: videojs.Player) => this.player = player)
252 this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations))
253
254 window[ 'videojsPlayer' ] = this.player
255
256 this.buildCSS()
257
258 await this.buildDock(videoInfo, configResponse)
259
260 this.initializeApi()
261
262 this.removePlaceholder()
263 }
264
265 private handleError (err: Error, translations?: { [ id: string ]: string }) {
266 if (err.message.indexOf('from xs param') !== -1) {
267 this.player.dispose()
268 this.videoElement = null
269 this.displayError('This video is not available because the remote instance is not responding.', translations)
270 return
271 }
272 }
273
274 private async buildDock (videoInfo: VideoDetails, configResponse: Response) {
275 if (!this.controls) return
276
277 // On webtorrent fallback, player may have been disposed
278 if (!this.player.player_) return
279
280 const title = this.title ? videoInfo.name : undefined
281
282 const config: ServerConfig = await configResponse.json()
283 const description = config.tracker.enabled && this.warningTitle
284 ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>'
285 : undefined
286
287 this.player.dock({
288 title,
289 description
290 })
291 }
292
293 private buildCSS () {
294 const body = document.getElementById('custom-css')
295
296 if (this.bigPlayBackgroundColor) {
297 body.style.setProperty('--embedBigPlayBackgroundColor', this.bigPlayBackgroundColor)
298 }
299
300 if (this.foregroundColor) {
301 body.style.setProperty('--embedForegroundColor', this.foregroundColor)
302 }
303 }
304
305 private async buildCaptions (serverTranslations: any, captionsResponse: Response): Promise<VideoJSCaption[]> {
306 if (captionsResponse.ok) {
307 const { data } = (await captionsResponse.json()) as ResultList<VideoCaption>
308
309 return data.map(c => ({
310 label: peertubeTranslate(c.language.label, serverTranslations),
311 language: c.language.id,
312 src: window.location.origin + c.captionPath
313 }))
314 }
315
316 return []
317 }
318
319 private loadPlaceholder (video: VideoDetails) {
320 const placeholder = this.getPlaceholderElement()
321
322 const url = window.location.origin + video.previewPath
323 placeholder.style.backgroundImage = `url("${url}")`
324 }
325
326 private removePlaceholder () {
327 const placeholder = this.getPlaceholderElement()
328 placeholder.parentElement.removeChild(placeholder)
329 }
330
331 private getPlaceholderElement () {
332 return document.getElementById('placeholder-preview')
333 }
334 }
335
336 PeerTubeEmbed.main()
337 .catch(err => console.error('Cannot init embed.', err))