embed.ts 13 KB

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