aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/standalone/videos
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-06-29 15:55:00 +0200
committerChocobozzz <me@florianbigard.com>2023-07-10 16:08:28 +0200
commita1bd2b77d99cec5c27d38501f5f12f9dc339de17 (patch)
tree58b4666297e0b832a52f962541498af61319110a /client/src/standalone/videos
parent8ef866071f8109719e68647141d4c9e138438585 (diff)
downloadPeerTube-a1bd2b77d99cec5c27d38501f5f12f9dc339de17.tar.gz
PeerTube-a1bd2b77d99cec5c27d38501f5f12f9dc339de17.tar.zst
PeerTube-a1bd2b77d99cec5c27d38501f5f12f9dc339de17.zip
Remove webtorrent support from client
Diffstat (limited to 'client/src/standalone/videos')
-rw-r--r--client/src/standalone/videos/embed-api.ts17
-rw-r--r--client/src/standalone/videos/embed.html8
-rw-r--r--client/src/standalone/videos/embed.ts225
-rw-r--r--client/src/standalone/videos/shared/index.ts2
-rw-r--r--client/src/standalone/videos/shared/player-html.ts19
-rw-r--r--client/src/standalone/videos/shared/player-options-builder.ts (renamed from client/src/standalone/videos/shared/player-manager-options.ts)261
6 files changed, 251 insertions, 281 deletions
diff --git a/client/src/standalone/videos/embed-api.ts b/client/src/standalone/videos/embed-api.ts
index a99f1edae..cdda122b2 100644
--- a/client/src/standalone/videos/embed-api.ts
+++ b/client/src/standalone/videos/embed-api.ts
@@ -72,15 +72,12 @@ export class PeerTubeEmbedApi {
72 private setResolution (resolutionId: number) { 72 private setResolution (resolutionId: number) {
73 logger.info(`Set resolution ${resolutionId}`) 73 logger.info(`Set resolution ${resolutionId}`)
74 74
75 if (this.isWebtorrent()) { 75 if (this.isWebVideo() && resolutionId === -1) {
76 if (resolutionId === -1 && this.embed.player.webtorrent().isAutoResolutionPossible() === false) return 76 logger.error('Auto resolution cannot be set in web video player mode')
77
78 this.embed.player.webtorrent().changeQuality(resolutionId)
79
80 return 77 return
81 } 78 }
82 79
83 this.embed.player.p2pMediaLoader().getHLSJS().currentLevel = resolutionId 80 this.embed.player.peertubeResolutions().select({ id: resolutionId, fireCallback: true })
84 } 81 }
85 82
86 private getCaptions (): PeerTubeTextTrack[] { 83 private getCaptions (): PeerTubeTextTrack[] {
@@ -152,8 +149,8 @@ export class PeerTubeEmbedApi {
152 // --------------------------------------------------------------------------- 149 // ---------------------------------------------------------------------------
153 150
154 // PeerTube specific capabilities 151 // PeerTube specific capabilities
155 this.embed.player.peertubeResolutions().on('resolutionsAdded', () => this.loadResolutions()) 152 this.embed.player.peertubeResolutions().on('resolutions-added', () => this.loadResolutions())
156 this.embed.player.peertubeResolutions().on('resolutionChanged', () => this.loadResolutions()) 153 this.embed.player.peertubeResolutions().on('resolutions-changed', () => this.loadResolutions())
157 154
158 this.loadResolutions() 155 this.loadResolutions()
159 156
@@ -193,7 +190,7 @@ export class PeerTubeEmbedApi {
193 }) 190 })
194 } 191 }
195 192
196 private isWebtorrent () { 193 private isWebVideo () {
197 return !!this.embed.player.webtorrent 194 return !!this.embed.player.webVideo
198 } 195 }
199} 196}
diff --git a/client/src/standalone/videos/embed.html b/client/src/standalone/videos/embed.html
index a74bb4cee..e2dc02b60 100644
--- a/client/src/standalone/videos/embed.html
+++ b/client/src/standalone/videos/embed.html
@@ -44,11 +44,11 @@
44 <div id="video-password-block"> 44 <div id="video-password-block">
45 <!-- eslint-disable-next-line @angular-eslint/template/elements-content --> 45 <!-- eslint-disable-next-line @angular-eslint/template/elements-content -->
46 <h1 id="video-password-title"></h1> 46 <h1 id="video-password-title"></h1>
47 47
48 <div id="video-password-content"></div> 48 <div id="video-password-content"></div>
49 49
50 <form id="video-password-form"> 50 <form id="video-password-form">
51 <input type="password" id="video-password-input" name="video-password" required> 51 <input type="password" id="video-password-input" name="video-password" autocomplete="user-password" required>
52 <button type="submit" id="video-password-submit"> </button> 52 <button type="submit" id="video-password-submit"> </button>
53 </form> 53 </form>
54 54
@@ -60,8 +60,6 @@
60 60
61 <div id="video-wrapper"></div> 61 <div id="video-wrapper"></div>
62 62
63 <div id="placeholder-preview"></div>
64
65 <script type="text/javascript"> 63 <script type="text/javascript">
66 // Can be called in embed.ts 64 // Can be called in embed.ts
67 window.displayIncompatibleBrowser = function () { 65 window.displayIncompatibleBrowser = function () {
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index 6e37ce193..5c68aaaf2 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -3,7 +3,6 @@ import '../../assets/player/shared/dock/peertube-dock-component'
3import '../../assets/player/shared/dock/peertube-dock-plugin' 3import '../../assets/player/shared/dock/peertube-dock-plugin'
4import { PeerTubeServerError } from 'src/types' 4import { PeerTubeServerError } from 'src/types'
5import videojs from 'video.js' 5import videojs from 'video.js'
6import { peertubeTranslate } from '../../../../shared/core-utils/i18n'
7import { 6import {
8 HTMLServerConfig, 7 HTMLServerConfig,
9 ResultList, 8 ResultList,
@@ -13,7 +12,7 @@ import {
13 VideoPlaylistElement, 12 VideoPlaylistElement,
14 VideoState 13 VideoState
15} from '../../../../shared/models' 14} from '../../../../shared/models'
16import { PeertubePlayerManager } from '../../assets/player' 15import { PeerTubePlayer } from '../../assets/player/peertube-player'
17import { TranslationsManager } from '../../assets/player/translations-manager' 16import { TranslationsManager } from '../../assets/player/translations-manager'
18import { getParamString, logger, videoRequiresFileToken } from '../../root-helpers' 17import { getParamString, logger, videoRequiresFileToken } from '../../root-helpers'
19import { PeerTubeEmbedApi } from './embed-api' 18import { PeerTubeEmbedApi } from './embed-api'
@@ -21,7 +20,7 @@ import {
21 AuthHTTP, 20 AuthHTTP,
22 LiveManager, 21 LiveManager,
23 PeerTubePlugin, 22 PeerTubePlugin,
24 PlayerManagerOptions, 23 PlayerOptionsBuilder,
25 PlaylistFetcher, 24 PlaylistFetcher,
26 PlaylistTracker, 25 PlaylistTracker,
27 Translations, 26 Translations,
@@ -36,17 +35,23 @@ export class PeerTubeEmbed {
36 config: HTMLServerConfig 35 config: HTMLServerConfig
37 36
38 private translationsPromise: Promise<{ [id: string]: string }> 37 private translationsPromise: Promise<{ [id: string]: string }>
39 private PeertubePlayerManagerModulePromise: Promise<any> 38 private PeerTubePlayerManagerModulePromise: Promise<any>
40 39
41 private readonly http: AuthHTTP 40 private readonly http: AuthHTTP
42 private readonly videoFetcher: VideoFetcher 41 private readonly videoFetcher: VideoFetcher
43 private readonly playlistFetcher: PlaylistFetcher 42 private readonly playlistFetcher: PlaylistFetcher
44 private readonly peertubePlugin: PeerTubePlugin 43 private readonly peertubePlugin: PeerTubePlugin
45 private readonly playerHTML: PlayerHTML 44 private readonly playerHTML: PlayerHTML
46 private readonly playerManagerOptions: PlayerManagerOptions 45 private readonly playerOptionsBuilder: PlayerOptionsBuilder
47 private readonly liveManager: LiveManager 46 private readonly liveManager: LiveManager
48 47
48 private peertubePlayer: PeerTubePlayer
49
49 private playlistTracker: PlaylistTracker 50 private playlistTracker: PlaylistTracker
51
52 private alreadyInitialized = false
53 private alreadyPlayed = false
54
50 private videoPassword: string 55 private videoPassword: string
51 private requiresPassword: boolean 56 private requiresPassword: boolean
52 57
@@ -59,7 +64,7 @@ export class PeerTubeEmbed {
59 this.playlistFetcher = new PlaylistFetcher(this.http) 64 this.playlistFetcher = new PlaylistFetcher(this.http)
60 this.peertubePlugin = new PeerTubePlugin(this.http) 65 this.peertubePlugin = new PeerTubePlugin(this.http)
61 this.playerHTML = new PlayerHTML(videoWrapperId) 66 this.playerHTML = new PlayerHTML(videoWrapperId)
62 this.playerManagerOptions = new PlayerManagerOptions(this.playerHTML, this.videoFetcher, this.peertubePlugin) 67 this.playerOptionsBuilder = new PlayerOptionsBuilder(this.playerHTML, this.videoFetcher, this.peertubePlugin)
63 this.liveManager = new LiveManager(this.playerHTML) 68 this.liveManager = new LiveManager(this.playerHTML)
64 this.requiresPassword = false 69 this.requiresPassword = false
65 70
@@ -81,14 +86,14 @@ export class PeerTubeEmbed {
81 } 86 }
82 87
83 getScope () { 88 getScope () {
84 return this.playerManagerOptions.getScope() 89 return this.playerOptionsBuilder.getScope()
85 } 90 }
86 91
87 // --------------------------------------------------------------------------- 92 // ---------------------------------------------------------------------------
88 93
89 async init () { 94 async init () {
90 this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language) 95 this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language)
91 this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager') 96 this.PeerTubePlayerManagerModulePromise = import('../../assets/player/peertube-player')
92 97
93 // Issue when we parsed config from HTML, fallback to API 98 // Issue when we parsed config from HTML, fallback to API
94 if (!this.config) { 99 if (!this.config) {
@@ -102,7 +107,7 @@ export class PeerTubeEmbed {
102 107
103 if (!videoId) return 108 if (!videoId) return
104 109
105 return this.loadVideoAndBuildPlayer({ uuid: videoId, autoplayFromPreviousVideo: false, forceAutoplay: false }) 110 return this.loadVideoAndBuildPlayer({ uuid: videoId, forceAutoplay: false })
106 } 111 }
107 112
108 private async initPlaylist () { 113 private async initPlaylist () {
@@ -137,7 +142,7 @@ export class PeerTubeEmbed {
137 } 142 }
138 143
139 private initializeApi () { 144 private initializeApi () {
140 if (this.playerManagerOptions.hasAPIEnabled()) { 145 if (this.playerOptionsBuilder.hasAPIEnabled()) {
141 if (this.api) { 146 if (this.api) {
142 this.api.reInit() 147 this.api.reInit()
143 return 148 return
@@ -159,7 +164,7 @@ export class PeerTubeEmbed {
159 164
160 this.playlistTracker.setCurrentElement(next) 165 this.playlistTracker.setCurrentElement(next)
161 166
162 return this.loadVideoAndBuildPlayer({ uuid: next.video.uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }) 167 return this.loadVideoAndBuildPlayer({ uuid: next.video.uuid, forceAutoplay: false })
163 } 168 }
164 169
165 async playPreviousPlaylistVideo () { 170 async playPreviousPlaylistVideo () {
@@ -171,7 +176,7 @@ export class PeerTubeEmbed {
171 176
172 this.playlistTracker.setCurrentElement(previous) 177 this.playlistTracker.setCurrentElement(previous)
173 178
174 await this.loadVideoAndBuildPlayer({ uuid: previous.video.uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }) 179 await this.loadVideoAndBuildPlayer({ uuid: previous.video.uuid, forceAutoplay: false })
175 } 180 }
176 181
177 getCurrentPlaylistPosition () { 182 getCurrentPlaylistPosition () {
@@ -182,10 +187,9 @@ export class PeerTubeEmbed {
182 187
183 private async loadVideoAndBuildPlayer (options: { 188 private async loadVideoAndBuildPlayer (options: {
184 uuid: string 189 uuid: string
185 autoplayFromPreviousVideo: boolean
186 forceAutoplay: boolean 190 forceAutoplay: boolean
187 }) { 191 }) {
188 const { uuid, autoplayFromPreviousVideo, forceAutoplay } = options 192 const { uuid, forceAutoplay } = options
189 193
190 try { 194 try {
191 const { 195 const {
@@ -194,7 +198,7 @@ export class PeerTubeEmbed {
194 storyboardsPromise 198 storyboardsPromise
195 } = await this.videoFetcher.loadVideo({ videoId: uuid, videoPassword: this.videoPassword }) 199 } = await this.videoFetcher.loadVideo({ videoId: uuid, videoPassword: this.videoPassword })
196 200
197 return this.buildVideoPlayer({ videoResponse, captionsPromise, storyboardsPromise, autoplayFromPreviousVideo, forceAutoplay }) 201 return this.buildVideoPlayer({ videoResponse, captionsPromise, storyboardsPromise, forceAutoplay })
198 } catch (err) { 202 } catch (err) {
199 203
200 if (await this.handlePasswordError(err)) this.loadVideoAndBuildPlayer({ ...options }) 204 if (await this.handlePasswordError(err)) this.loadVideoAndBuildPlayer({ ...options })
@@ -206,20 +210,14 @@ export class PeerTubeEmbed {
206 videoResponse: Response 210 videoResponse: Response
207 storyboardsPromise: Promise<Response> 211 storyboardsPromise: Promise<Response>
208 captionsPromise: Promise<Response> 212 captionsPromise: Promise<Response>
209 autoplayFromPreviousVideo: boolean
210 forceAutoplay: boolean 213 forceAutoplay: boolean
211 }) { 214 }) {
212 const { videoResponse, captionsPromise, storyboardsPromise, autoplayFromPreviousVideo, forceAutoplay } = options 215 const { videoResponse, captionsPromise, storyboardsPromise, forceAutoplay } = options
213
214 this.resetPlayerElement()
215 216
216 const videoInfoPromise = videoResponse.json() 217 const videoInfoPromise = videoResponse.json()
217 .then(async (videoInfo: VideoDetails) => { 218 .then(async (videoInfo: VideoDetails) => {
218 this.playerManagerOptions.loadParams(this.config, videoInfo) 219 this.playerOptionsBuilder.loadParams(this.config, videoInfo)
219 220
220 if (!autoplayFromPreviousVideo && !this.playerManagerOptions.hasAutoplay()) {
221 this.playerHTML.buildPlaceholder(videoInfo)
222 }
223 const live = videoInfo.isLive 221 const live = videoInfo.isLive
224 ? await this.videoFetcher.loadLive(videoInfo) 222 ? await this.videoFetcher.loadLive(videoInfo)
225 : undefined 223 : undefined
@@ -235,89 +233,75 @@ export class PeerTubeEmbed {
235 { video, live, videoFileToken }, 233 { video, live, videoFileToken },
236 translations, 234 translations,
237 captionsResponse, 235 captionsResponse,
238 storyboardsResponse, 236 storyboardsResponse
239 PeertubePlayerManagerModule
240 ] = await Promise.all([ 237 ] = await Promise.all([
241 videoInfoPromise, 238 videoInfoPromise,
242 this.translationsPromise, 239 this.translationsPromise,
243 captionsPromise, 240 captionsPromise,
244 storyboardsPromise, 241 storyboardsPromise,
245 this.PeertubePlayerManagerModulePromise 242 this.buildPlayerIfNeeded()
246 ]) 243 ])
247 244
248 await this.peertubePlugin.loadPlugins(this.config, translations) 245 this.peertubePlayer.setPoster(window.location.origin + video.previewPath)
246
247 const playlist = this.playlistTracker
248 ? {
249 onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, forceAutoplay: false }),
249 250
250 const PlayerManager: typeof PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager 251 playlistTracker: this.playlistTracker,
252 playNext: () => this.playNextPlaylistVideo(),
253 playPrevious: () => this.playPreviousPlaylistVideo()
254 }
255 : undefined
251 256
252 const playerOptions = await this.playerManagerOptions.getPlayerOptions({ 257 const loadOptions = await this.playerOptionsBuilder.getPlayerLoadOptions({
253 video, 258 video,
254 captionsResponse, 259 captionsResponse,
255 autoplayFromPreviousVideo,
256 translations, 260 translations,
257 serverConfig: this.config,
258 261
259 storyboardsResponse, 262 storyboardsResponse,
260 263
261 authorizationHeader: () => this.http.getHeaderTokenValue(),
262 videoFileToken: () => videoFileToken, 264 videoFileToken: () => videoFileToken,
263 videoPassword: () => this.videoPassword, 265 videoPassword: () => this.videoPassword,
264 requiresPassword: this.requiresPassword, 266 requiresPassword: this.requiresPassword,
265 267
266 onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }), 268 playlist,
267
268 playlistTracker: this.playlistTracker,
269 playNextPlaylistVideo: () => this.playNextPlaylistVideo(),
270 playPreviousPlaylistVideo: () => this.playPreviousPlaylistVideo(),
271 269
272 live, 270 live,
273 forceAutoplay 271 forceAutoplay,
272 alreadyPlayed: this.alreadyPlayed
274 }) 273 })
274 await this.peertubePlayer.load(loadOptions)
275 275
276 this.player = await PlayerManager.initialize(this.playerManagerOptions.getMode(), playerOptions, (player: videojs.Player) => { 276 if (!this.alreadyInitialized) {
277 this.player = player 277 this.player = this.peertubePlayer.getPlayer();
278 })
279 278
280 this.player.on('customError', (event: any, data: any) => { 279 (window as any)['videojsPlayer'] = this.player
281 const message = data?.err?.message || ''
282 if (!message.includes('from xs param')) return
283 280
284 this.player.dispose() 281 this.buildCSS()
285 this.playerHTML.removePlayerElement() 282 this.initializeApi()
286 this.playerHTML.displayError('This video is not available because the remote instance is not responding.', translations) 283 }
287 });
288 284
289 (window as any)['videojsPlayer'] = this.player 285 this.alreadyInitialized = true
290 286
291 this.buildCSS() 287 this.player.one('play', () => {
292 this.buildPlayerDock(video) 288 this.alreadyPlayed = true
293 this.initializeApi() 289 })
294 290
295 this.playerHTML.removePlaceholder()
296 if (this.videoPassword) this.playerHTML.removeVideoPasswordBlock() 291 if (this.videoPassword) this.playerHTML.removeVideoPasswordBlock()
297 292
298 if (this.isPlaylistEmbed()) {
299 await this.buildPlayerPlaylistUpnext()
300
301 this.player.playlist().updateSelected()
302
303 this.player.on('stopped', () => {
304 this.playNextPlaylistVideo()
305 })
306 }
307
308 if (video.isLive) { 293 if (video.isLive) {
309 this.liveManager.listenForChanges({ 294 this.liveManager.listenForChanges({
310 video, 295 video,
311 onPublishedVideo: () => { 296 onPublishedVideo: () => {
312 this.liveManager.stopListeningForChanges(video) 297 this.liveManager.stopListeningForChanges(video)
313 this.loadVideoAndBuildPlayer({ uuid: video.uuid, autoplayFromPreviousVideo: false, forceAutoplay: true }) 298 this.loadVideoAndBuildPlayer({ uuid: video.uuid, forceAutoplay: true })
314 } 299 }
315 }) 300 })
316 301
317 if (video.state.id === VideoState.WAITING_FOR_LIVE || video.state.id === VideoState.LIVE_ENDED) { 302 if (video.state.id === VideoState.WAITING_FOR_LIVE || video.state.id === VideoState.LIVE_ENDED) {
318 this.liveManager.displayInfo({ state: video.state.id, translations }) 303 this.liveManager.displayInfo({ state: video.state.id, translations })
319 304 this.peertubePlayer.disable()
320 this.disablePlayer()
321 } else { 305 } else {
322 this.correctlyHandleLiveEnding(translations) 306 this.correctlyHandleLiveEnding(translations)
323 } 307 }
@@ -326,74 +310,15 @@ export class PeerTubeEmbed {
326 this.peertubePlugin.getPluginsManager().runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video }) 310 this.peertubePlugin.getPluginsManager().runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video })
327 } 311 }
328 312
329 private resetPlayerElement () {
330 if (this.player) {
331 this.player.dispose()
332 this.player = undefined
333 }
334
335 const playerElement = document.createElement('video')
336 playerElement.className = 'video-js vjs-peertube-skin'
337 playerElement.setAttribute('playsinline', 'true')
338
339 this.playerHTML.setPlayerElement(playerElement)
340 this.playerHTML.addPlayerElementToDOM()
341 }
342
343 private async buildPlayerPlaylistUpnext () {
344 const translations = await this.translationsPromise
345
346 this.player.upnext({
347 timeout: 10000, // 10s
348 headText: peertubeTranslate('Up Next', translations),
349 cancelText: peertubeTranslate('Cancel', translations),
350 suspendedText: peertubeTranslate('Autoplay is suspended', translations),
351 getTitle: () => this.playlistTracker.nextVideoTitle(),
352 next: () => this.playNextPlaylistVideo(),
353 condition: () => !!this.playlistTracker.getNextPlaylistElement(),
354 suspended: () => false
355 })
356 }
357
358 private buildPlayerDock (videoInfo: VideoDetails) {
359 if (!this.playerManagerOptions.hasControls()) return
360
361 // On webtorrent fallback, player may have been disposed
362 if (!this.player.player_) return
363
364 const title = this.playerManagerOptions.hasTitle()
365 ? videoInfo.name
366 : undefined
367
368 const description = this.playerManagerOptions.hasWarningTitle() && this.playerManagerOptions.hasP2PEnabled()
369 ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>'
370 : undefined
371
372 if (!title && !description) return
373
374 const availableAvatars = videoInfo.channel.avatars.filter(a => a.width < 50)
375 const avatar = availableAvatars.length !== 0
376 ? availableAvatars[0]
377 : undefined
378
379 this.player.peertubeDock({
380 title,
381 description,
382 avatarUrl: title && avatar
383 ? avatar.path
384 : undefined
385 })
386 }
387
388 private buildCSS () { 313 private buildCSS () {
389 const body = document.getElementById('custom-css') 314 const body = document.getElementById('custom-css')
390 315
391 if (this.playerManagerOptions.hasBigPlayBackgroundColor()) { 316 if (this.playerOptionsBuilder.hasBigPlayBackgroundColor()) {
392 body.style.setProperty('--embedBigPlayBackgroundColor', this.playerManagerOptions.getBigPlayBackgroundColor()) 317 body.style.setProperty('--embedBigPlayBackgroundColor', this.playerOptionsBuilder.getBigPlayBackgroundColor())
393 } 318 }
394 319
395 if (this.playerManagerOptions.hasForegroundColor()) { 320 if (this.playerOptionsBuilder.hasForegroundColor()) {
396 body.style.setProperty('--embedForegroundColor', this.playerManagerOptions.getForegroundColor()) 321 body.style.setProperty('--embedForegroundColor', this.playerOptionsBuilder.getForegroundColor())
397 } 322 }
398 } 323 }
399 324
@@ -415,23 +340,10 @@ export class PeerTubeEmbed {
415 // Display the live ended information 340 // Display the live ended information
416 this.liveManager.displayInfo({ state: VideoState.LIVE_ENDED, translations }) 341 this.liveManager.displayInfo({ state: VideoState.LIVE_ENDED, translations })
417 342
418 this.disablePlayer() 343 this.peertubePlayer.disable()
419 }) 344 })
420 } 345 }
421 346
422 private disablePlayer () {
423 if (this.player.isFullscreen()) {
424 this.player.exitFullscreen()
425 }
426
427 // Disable player
428 this.player.hasStarted(false)
429 this.player.removeClass('vjs-has-autoplay')
430 this.player.bigPlayButton.hide();
431
432 (this.player.el() as HTMLElement).style.pointerEvents = 'none'
433 }
434
435 private async handlePasswordError (err: PeerTubeServerError) { 347 private async handlePasswordError (err: PeerTubeServerError) {
436 let incorrectPassword: boolean = null 348 let incorrectPassword: boolean = null
437 if (err.serverCode === ServerErrorCode.VIDEO_REQUIRES_PASSWORD) incorrectPassword = false 349 if (err.serverCode === ServerErrorCode.VIDEO_REQUIRES_PASSWORD) incorrectPassword = false
@@ -447,6 +359,33 @@ export class PeerTubeEmbed {
447 return true 359 return true
448 } 360 }
449 361
362 private async buildPlayerIfNeeded () {
363 if (this.peertubePlayer) {
364 this.peertubePlayer.enable()
365
366 return
367 }
368
369 const playerElement = document.createElement('video')
370 playerElement.className = 'video-js vjs-peertube-skin'
371 playerElement.setAttribute('playsinline', 'true')
372
373 this.playerHTML.setPlayerElement(playerElement)
374 this.playerHTML.addPlayerElementToDOM()
375
376 const [ { PeerTubePlayer } ] = await Promise.all([
377 this.PeerTubePlayerManagerModulePromise,
378 this.peertubePlugin.loadPlugins(this.config, await this.translationsPromise)
379 ])
380
381 const constructorOptions = this.playerOptionsBuilder.getPlayerConstructorOptions({
382 serverConfig: this.config,
383 authorizationHeader: () => this.http.getHeaderTokenValue()
384 })
385 this.peertubePlayer = new PeerTubePlayer(constructorOptions)
386
387 this.player = this.peertubePlayer.getPlayer()
388 }
450} 389}
451 390
452PeerTubeEmbed.main() 391PeerTubeEmbed.main()
diff --git a/client/src/standalone/videos/shared/index.ts b/client/src/standalone/videos/shared/index.ts
index 928b8e270..dcc522ac6 100644
--- a/client/src/standalone/videos/shared/index.ts
+++ b/client/src/standalone/videos/shared/index.ts
@@ -2,7 +2,7 @@ export * from './auth-http'
2export * from './peertube-plugin' 2export * from './peertube-plugin'
3export * from './live-manager' 3export * from './live-manager'
4export * from './player-html' 4export * from './player-html'
5export * from './player-manager-options' 5export * from './player-options-builder'
6export * from './playlist-fetcher' 6export * from './playlist-fetcher'
7export * from './playlist-tracker' 7export * from './playlist-tracker'
8export * from './translations' 8export * from './translations'
diff --git a/client/src/standalone/videos/shared/player-html.ts b/client/src/standalone/videos/shared/player-html.ts
index a0846d9d7..0defa0d70 100644
--- a/client/src/standalone/videos/shared/player-html.ts
+++ b/client/src/standalone/videos/shared/player-html.ts
@@ -1,5 +1,4 @@
1import { peertubeTranslate } from '../../../../../shared/core-utils/i18n' 1import { peertubeTranslate } from '../../../../../shared/core-utils/i18n'
2import { VideoDetails } from '../../../../../shared/models'
3import { logger } from '../../../root-helpers' 2import { logger } from '../../../root-helpers'
4import { Translations } from './translations' 3import { Translations } from './translations'
5 4
@@ -59,7 +58,6 @@ export class PlayerHTML {
59 const { incorrectPassword, translations } = options 58 const { incorrectPassword, translations } = options
60 return new Promise((resolve) => { 59 return new Promise((resolve) => {
61 60
62 this.removePlaceholder()
63 this.wrapperElement.style.display = 'none' 61 this.wrapperElement.style.display = 'none'
64 62
65 const translatedTitle = peertubeTranslate('This video is password protected', translations) 63 const translatedTitle = peertubeTranslate('This video is password protected', translations)
@@ -107,19 +105,6 @@ export class PlayerHTML {
107 this.wrapperElement.style.display = 'block' 105 this.wrapperElement.style.display = 'block'
108 } 106 }
109 107
110 buildPlaceholder (video: VideoDetails) {
111 const placeholder = this.getPlaceholderElement()
112
113 const url = window.location.origin + video.previewPath
114 placeholder.style.backgroundImage = `url("${url}")`
115 placeholder.style.display = 'block'
116 }
117
118 removePlaceholder () {
119 const placeholder = this.getPlaceholderElement()
120 placeholder.style.display = 'none'
121 }
122
123 displayInformation (text: string, translations: Translations) { 108 displayInformation (text: string, translations: Translations) {
124 if (this.informationElement) this.removeInformation() 109 if (this.informationElement) this.removeInformation()
125 110
@@ -137,10 +122,6 @@ export class PlayerHTML {
137 this.informationElement = undefined 122 this.informationElement = undefined
138 } 123 }
139 124
140 private getPlaceholderElement () {
141 return document.getElementById('placeholder-preview')
142 }
143
144 private removeElement (element: HTMLElement) { 125 private removeElement (element: HTMLElement) {
145 element.parentElement.removeChild(element) 126 element.parentElement.removeChild(element)
146 } 127 }
diff --git a/client/src/standalone/videos/shared/player-manager-options.ts b/client/src/standalone/videos/shared/player-options-builder.ts
index 3c7521bc2..8a4e32444 100644
--- a/client/src/standalone/videos/shared/player-manager-options.ts
+++ b/client/src/standalone/videos/shared/player-options-builder.ts
@@ -10,7 +10,7 @@ import {
10 VideoState, 10 VideoState,
11 VideoStreamingPlaylistType 11 VideoStreamingPlaylistType
12} from '../../../../../shared/models' 12} from '../../../../../shared/models'
13import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode, VideoJSCaption } from '../../../assets/player' 13import { HLSOptions, PeerTubePlayerContructorOptions, PeerTubePlayerLoadOptions, PlayerMode, VideoJSCaption } from '../../../assets/player'
14import { 14import {
15 getBoolOrDefault, 15 getBoolOrDefault,
16 getParamString, 16 getParamString,
@@ -27,7 +27,7 @@ import { PlaylistTracker } from './playlist-tracker'
27import { Translations } from './translations' 27import { Translations } from './translations'
28import { VideoFetcher } from './video-fetcher' 28import { VideoFetcher } from './video-fetcher'
29 29
30export class PlayerManagerOptions { 30export class PlayerOptionsBuilder {
31 private autoplay: boolean 31 private autoplay: boolean
32 32
33 private controls: boolean 33 private controls: boolean
@@ -141,10 +141,10 @@ export class PlayerManagerOptions {
141 141
142 if (modeParam) { 142 if (modeParam) {
143 if (modeParam === 'p2p-media-loader') this.mode = 'p2p-media-loader' 143 if (modeParam === 'p2p-media-loader') this.mode = 'p2p-media-loader'
144 else this.mode = 'webtorrent' 144 else this.mode = 'web-video'
145 } else { 145 } else {
146 if (Array.isArray(video.streamingPlaylists) && video.streamingPlaylists.length !== 0) this.mode = 'p2p-media-loader' 146 if (Array.isArray(video.streamingPlaylists) && video.streamingPlaylists.length !== 0) this.mode = 'p2p-media-loader'
147 else this.mode = 'webtorrent' 147 else this.mode = 'web-video'
148 } 148 }
149 } catch (err) { 149 } catch (err) {
150 logger.error('Cannot get params from URL.', err) 150 logger.error('Cannot get params from URL.', err)
@@ -153,7 +153,47 @@ export class PlayerManagerOptions {
153 153
154 // --------------------------------------------------------------------------- 154 // ---------------------------------------------------------------------------
155 155
156 async getPlayerOptions (options: { 156 getPlayerConstructorOptions (options: {
157 serverConfig: HTMLServerConfig
158 authorizationHeader: () => string
159 }): PeerTubePlayerContructorOptions {
160 const { serverConfig, authorizationHeader } = options
161
162 return {
163 controls: this.controls,
164 controlBar: this.controlBar,
165
166 muted: this.muted,
167 loop: this.loop,
168
169 playbackRate: this.playbackRate,
170
171 inactivityTimeout: 2500,
172 videoViewIntervalMs: 5000,
173 metricsUrl: window.location.origin + '/api/v1/metrics/playback',
174
175 authorizationHeader,
176
177 playerElement: () => this.playerHTML.getPlayerElement(),
178 enableHotkeys: true,
179
180 peertubeLink: () => this.peertubeLink,
181 instanceName: serverConfig.instance.name,
182
183 theaterButton: false,
184
185 serverUrl: window.location.origin,
186 language: navigator.language,
187
188 pluginsManager: this.peertubePlugin.getPluginsManager(),
189
190 errorNotifier: () => {
191 // Empty, we don't have a notifier in the embed
192 }
193 }
194 }
195
196 async getPlayerLoadOptions (options: {
157 video: VideoDetails 197 video: VideoDetails
158 captionsResponse: Response 198 captionsResponse: Response
159 199
@@ -161,39 +201,35 @@ export class PlayerManagerOptions {
161 201
162 live?: LiveVideo 202 live?: LiveVideo
163 203
204 alreadyPlayed: boolean
164 forceAutoplay: boolean 205 forceAutoplay: boolean
165 206
166 authorizationHeader: () => string
167 videoFileToken: () => string 207 videoFileToken: () => string
168 208
169 videoPassword: () => string 209 videoPassword: () => string
170 requiresPassword: boolean 210 requiresPassword: boolean
171 211
172 serverConfig: HTMLServerConfig
173
174 autoplayFromPreviousVideo: boolean
175
176 translations: Translations 212 translations: Translations
177 213
178 playlistTracker?: PlaylistTracker 214 playlist?: {
179 playNextPlaylistVideo?: () => any 215 playlistTracker: PlaylistTracker
180 playPreviousPlaylistVideo?: () => any 216 playNext: () => any
181 onVideoUpdate?: (uuid: string) => any 217 playPrevious: () => any
182 }) { 218 onVideoUpdate: (uuid: string) => any
219 }
220 }): Promise<PeerTubePlayerLoadOptions> {
183 const { 221 const {
184 video, 222 video,
185 captionsResponse, 223 captionsResponse,
186 autoplayFromPreviousVideo,
187 videoFileToken, 224 videoFileToken,
188 videoPassword, 225 videoPassword,
189 requiresPassword, 226 requiresPassword,
190 translations, 227 translations,
228 alreadyPlayed,
191 forceAutoplay, 229 forceAutoplay,
192 playlistTracker, 230 playlist,
193 live, 231 live,
194 storyboardsResponse, 232 storyboardsResponse
195 authorizationHeader,
196 serverConfig
197 } = options 233 } = options
198 234
199 const [ videoCaptions, storyboard ] = await Promise.all([ 235 const [ videoCaptions, storyboard ] = await Promise.all([
@@ -201,88 +237,56 @@ export class PlayerManagerOptions {
201 this.buildStoryboard(storyboardsResponse) 237 this.buildStoryboard(storyboardsResponse)
202 ]) 238 ])
203 239
204 const playerOptions: PeertubePlayerManagerOptions = { 240 return {
205 common: { 241 mode: this.mode,
206 // Autoplay in playlist mode
207 autoplay: autoplayFromPreviousVideo ? true : this.autoplay,
208 forceAutoplay,
209
210 controls: this.controls,
211 controlBar: this.controlBar,
212
213 muted: this.muted,
214 loop: this.loop,
215
216 p2pEnabled: this.p2pEnabled,
217
218 captions: videoCaptions.length !== 0,
219 subtitle: this.subtitle,
220 242
221 storyboard, 243 autoplay: forceAutoplay || alreadyPlayed || this.autoplay,
244 forceAutoplay,
222 245
223 startTime: playlistTracker 246 p2pEnabled: this.p2pEnabled,
224 ? playlistTracker.getCurrentElement().startTimestamp
225 : this.startTime,
226 stopTime: playlistTracker
227 ? playlistTracker.getCurrentElement().stopTimestamp
228 : this.stopTime,
229 247
230 playbackRate: this.playbackRate, 248 subtitle: this.subtitle,
231 249
232 videoCaptions, 250 storyboard,
233 inactivityTimeout: 2500,
234 videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid),
235 videoViewIntervalMs: 5000,
236 metricsUrl: window.location.origin + '/api/v1/metrics/playback',
237 251
238 videoShortUUID: video.shortUUID, 252 startTime: playlist
239 videoUUID: video.uuid, 253 ? playlist.playlistTracker.getCurrentElement().startTimestamp
254 : this.startTime,
255 stopTime: playlist
256 ? playlist.playlistTracker.getCurrentElement().stopTimestamp
257 : this.stopTime,
240 258
241 playerElement: this.playerHTML.getPlayerElement(), 259 videoCaptions,
242 onPlayerElementChange: (element: HTMLVideoElement) => { 260 videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid),
243 this.playerHTML.setPlayerElement(element)
244 },
245 261
246 videoDuration: video.duration, 262 videoShortUUID: video.shortUUID,
247 enableHotkeys: true, 263 videoUUID: video.uuid,
248 264
249 peertubeLink: this.peertubeLink, 265 duration: video.duration,
250 instanceName: serverConfig.instance.name,
251 266
252 poster: window.location.origin + video.previewPath, 267 poster: window.location.origin + video.previewPath,
253 theaterButton: false,
254 268
255 serverUrl: window.location.origin, 269 embedUrl: window.location.origin + video.embedPath,
256 language: navigator.language, 270 embedTitle: video.name,
257 embedUrl: window.location.origin + video.embedPath,
258 embedTitle: video.name,
259 271
260 requiresUserAuth: videoRequiresUserAuth(video), 272 requiresUserAuth: videoRequiresUserAuth(video),
261 authorizationHeader, 273 videoFileToken,
262 videoFileToken,
263 274
264 requiresPassword, 275 requiresPassword,
265 videoPassword, 276 videoPassword,
266 277
267 errorNotifier: () => { 278 ...this.buildLiveOptions(video, live),
268 // Empty, we don't have a notifier in the embed
269 },
270 279
271 ...this.buildLiveOptions(video, live), 280 ...this.buildPlaylistOptions(playlist),
272 281
273 ...this.buildPlaylistOptions(options) 282 dock: this.buildDockOptions(video),
274 },
275 283
276 webtorrent: { 284 webVideo: {
277 videoFiles: video.files 285 videoFiles: video.files
278 }, 286 },
279 287
280 ...this.buildP2PMediaLoaderOptions(video), 288 hls: this.buildHLSOptions(video)
281
282 pluginsManager: this.peertubePlugin.getPluginsManager()
283 } 289 }
284
285 return playerOptions
286 } 290 }
287 291
288 private buildLiveOptions (video: VideoDetails, live: LiveVideo) { 292 private buildLiveOptions (video: VideoDetails, live: LiveVideo) {
@@ -308,15 +312,27 @@ export class PlayerManagerOptions {
308 } 312 }
309 } 313 }
310 314
311 private buildPlaylistOptions (options: { 315 private buildPlaylistOptions (options?: {
312 playlistTracker?: PlaylistTracker 316 playlistTracker: PlaylistTracker
313 playNextPlaylistVideo?: () => any 317 playNext: () => any
314 playPreviousPlaylistVideo?: () => any 318 playPrevious: () => any
315 onVideoUpdate?: (uuid: string) => any 319 onVideoUpdate: (uuid: string) => any
316 }) { 320 }) {
317 const { playlistTracker, playNextPlaylistVideo, playPreviousPlaylistVideo, onVideoUpdate } = options 321 if (!options) {
322 return {
323 nextVideo: {
324 enabled: false,
325 displayControlBarButton: false,
326 getVideoTitle: () => ''
327 },
328 previousVideo: {
329 enabled: false,
330 displayControlBarButton: false
331 }
332 }
333 }
318 334
319 if (!playlistTracker) return {} 335 const { playlistTracker, playNext, playPrevious, onVideoUpdate } = options
320 336
321 return { 337 return {
322 playlist: { 338 playlist: {
@@ -332,27 +348,37 @@ export class PlayerManagerOptions {
332 } 348 }
333 }, 349 },
334 350
335 nextVideo: () => playNextPlaylistVideo(), 351 previousVideo: {
336 hasNextVideo: () => playlistTracker.hasNextPlaylistElement(), 352 enabled: playlistTracker.hasPreviousPlaylistElement(),
353 handler: () => playPrevious(),
354 displayControlBarButton: true
355 },
356
357 nextVideo: {
358 enabled: playlistTracker.hasNextPlaylistElement(),
359 handler: () => playNext(),
360 getVideoTitle: () => playlistTracker.getNextPlaylistElement()?.video?.name,
361 displayControlBarButton: true
362 },
337 363
338 previousVideo: () => playPreviousPlaylistVideo(), 364 upnext: {
339 hasPreviousVideo: () => playlistTracker.hasPreviousPlaylistElement() 365 isEnabled: () => true,
366 isSuspended: () => false,
367 timeout: 0
368 }
340 } 369 }
341 } 370 }
342 371
343 private buildP2PMediaLoaderOptions (video: VideoDetails) { 372 private buildHLSOptions (video: VideoDetails): HLSOptions {
344 if (this.mode !== 'p2p-media-loader') return {}
345
346 const hlsPlaylist = video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) 373 const hlsPlaylist = video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
374 if (!hlsPlaylist) return undefined
347 375
348 return { 376 return {
349 p2pMediaLoader: { 377 playlistUrl: hlsPlaylist.playlistUrl,
350 playlistUrl: hlsPlaylist.playlistUrl, 378 segmentsSha256Url: hlsPlaylist.segmentsSha256Url,
351 segmentsSha256Url: hlsPlaylist.segmentsSha256Url, 379 redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl),
352 redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), 380 trackerAnnounce: video.trackerUrls,
353 trackerAnnounce: video.trackerUrls, 381 videoFiles: hlsPlaylist.files
354 videoFiles: hlsPlaylist.files
355 } as P2PMediaLoaderOptions
356 } 382 }
357 } 383 }
358 384
@@ -374,6 +400,35 @@ export class PlayerManagerOptions {
374 400
375 // --------------------------------------------------------------------------- 401 // ---------------------------------------------------------------------------
376 402
403 private buildDockOptions (videoInfo: VideoDetails) {
404 if (!this.hasControls()) return undefined
405
406 const title = this.hasTitle()
407 ? videoInfo.name
408 : undefined
409
410 const description = this.hasWarningTitle() && this.hasP2PEnabled()
411 ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>'
412 : undefined
413
414 if (!title && !description) return
415
416 const availableAvatars = videoInfo.channel.avatars.filter(a => a.width < 50)
417 const avatar = availableAvatars.length !== 0
418 ? availableAvatars[0]
419 : undefined
420
421 return {
422 title,
423 description,
424 avatarUrl: title && avatar
425 ? avatar.path
426 : undefined
427 }
428 }
429
430 // ---------------------------------------------------------------------------
431
377 private isP2PEnabled (config: HTMLServerConfig, video: Video) { 432 private isP2PEnabled (config: HTMLServerConfig, video: Video) {
378 const userP2PEnabled = getBoolOrDefault( 433 const userP2PEnabled = getBoolOrDefault(
379 peertubeLocalStorage.getItem(UserLocalStorageKeys.P2P_ENABLED), 434 peertubeLocalStorage.getItem(UserLocalStorageKeys.P2P_ENABLED),