aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/standalone
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/standalone')
-rw-r--r--client/src/standalone/embed-player-api/.npmignore (renamed from client/src/standalone/player/.npmignore)0
-rw-r--r--client/src/standalone/embed-player-api/README.md (renamed from client/src/standalone/player/README.md)0
-rw-r--r--client/src/standalone/embed-player-api/definitions.ts (renamed from client/src/standalone/player/definitions.ts)0
-rw-r--r--client/src/standalone/embed-player-api/events.ts (renamed from client/src/standalone/player/events.ts)0
-rw-r--r--client/src/standalone/embed-player-api/package.json (renamed from client/src/standalone/player/package.json)0
-rw-r--r--client/src/standalone/embed-player-api/player.ts (renamed from client/src/standalone/player/player.ts)0
-rw-r--r--client/src/standalone/embed-player-api/tsconfig.json (renamed from client/src/standalone/player/tsconfig.json)0
-rw-r--r--client/src/standalone/embed-player-api/webpack.config.js (renamed from client/src/standalone/player/webpack.config.js)0
-rw-r--r--client/src/standalone/videos/embed-api.ts19
-rw-r--r--client/src/standalone/videos/embed.html19
-rw-r--r--client/src/standalone/videos/embed.scss43
-rw-r--r--client/src/standalone/videos/embed.ts276
-rw-r--r--client/src/standalone/videos/shared/auth-http.ts10
-rw-r--r--client/src/standalone/videos/shared/index.ts2
-rw-r--r--client/src/standalone/videos/shared/player-html.ts59
-rw-r--r--client/src/standalone/videos/shared/player-options-builder.ts (renamed from client/src/standalone/videos/shared/player-manager-options.ts)281
-rw-r--r--client/src/standalone/videos/shared/video-fetcher.ts35
-rw-r--r--client/src/standalone/videos/test-embed.ts4
18 files changed, 460 insertions, 288 deletions
diff --git a/client/src/standalone/player/.npmignore b/client/src/standalone/embed-player-api/.npmignore
index 870b6315b..870b6315b 100644
--- a/client/src/standalone/player/.npmignore
+++ b/client/src/standalone/embed-player-api/.npmignore
diff --git a/client/src/standalone/player/README.md b/client/src/standalone/embed-player-api/README.md
index 7b47e8f02..7b47e8f02 100644
--- a/client/src/standalone/player/README.md
+++ b/client/src/standalone/embed-player-api/README.md
diff --git a/client/src/standalone/player/definitions.ts b/client/src/standalone/embed-player-api/definitions.ts
index 495f1a98c..495f1a98c 100644
--- a/client/src/standalone/player/definitions.ts
+++ b/client/src/standalone/embed-player-api/definitions.ts
diff --git a/client/src/standalone/player/events.ts b/client/src/standalone/embed-player-api/events.ts
index 77d21c78c..77d21c78c 100644
--- a/client/src/standalone/player/events.ts
+++ b/client/src/standalone/embed-player-api/events.ts
diff --git a/client/src/standalone/player/package.json b/client/src/standalone/embed-player-api/package.json
index b549fbf52..b549fbf52 100644
--- a/client/src/standalone/player/package.json
+++ b/client/src/standalone/embed-player-api/package.json
diff --git a/client/src/standalone/player/player.ts b/client/src/standalone/embed-player-api/player.ts
index 75487258b..75487258b 100644
--- a/client/src/standalone/player/player.ts
+++ b/client/src/standalone/embed-player-api/player.ts
diff --git a/client/src/standalone/player/tsconfig.json b/client/src/standalone/embed-player-api/tsconfig.json
index eecc63dfb..eecc63dfb 100644
--- a/client/src/standalone/player/tsconfig.json
+++ b/client/src/standalone/embed-player-api/tsconfig.json
diff --git a/client/src/standalone/player/webpack.config.js b/client/src/standalone/embed-player-api/webpack.config.js
index 48d350edf..48d350edf 100644
--- a/client/src/standalone/player/webpack.config.js
+++ b/client/src/standalone/embed-player-api/webpack.config.js
diff --git a/client/src/standalone/videos/embed-api.ts b/client/src/standalone/videos/embed-api.ts
index a99f1edae..6227c378e 100644
--- a/client/src/standalone/videos/embed-api.ts
+++ b/client/src/standalone/videos/embed-api.ts
@@ -1,7 +1,7 @@
1import './embed.scss' 1import './embed.scss'
2import * as Channel from 'jschannel' 2import * as Channel from 'jschannel'
3import { logger } from '../../root-helpers' 3import { logger } from '../../root-helpers'
4import { PeerTubeResolution, PeerTubeTextTrack } from '../player/definitions' 4import { PeerTubeResolution, PeerTubeTextTrack } from '../embed-player-api/definitions'
5import { PeerTubeEmbed } from './embed' 5import { PeerTubeEmbed } from './embed'
6 6
7/** 7/**
@@ -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 32bf5f655..e2dc02b60 100644
--- a/client/src/standalone/videos/embed.html
+++ b/client/src/standalone/videos/embed.html
@@ -41,9 +41,24 @@
41 <div id="error-content"></div> 41 <div id="error-content"></div>
42 </div> 42 </div>
43 43
44 <div id="video-wrapper"></div> 44 <div id="video-password-block">
45 <!-- eslint-disable-next-line @angular-eslint/template/elements-content -->
46 <h1 id="video-password-title"></h1>
47
48 <div id="video-password-content"></div>
49
50 <form id="video-password-form">
51 <input type="password" id="video-password-input" name="video-password" autocomplete="user-password" required>
52 <button type="submit" id="video-password-submit"> </button>
53 </form>
45 54
46 <div id="placeholder-preview"></div> 55 <div id="video-password-error"></div>
56 <svg xmlns="http://www.w3.org/2000/svg" width="4rem" height="4rem" viewBox="0 0 24 24">
57 <g fill="none" stroke="#c4c4c4" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></g>
58 </svg>
59 </div>
60
61 <div id="video-wrapper"></div>
47 62
48 <script type="text/javascript"> 63 <script type="text/javascript">
49 // Can be called in embed.ts 64 // Can be called in embed.ts
diff --git a/client/src/standalone/videos/embed.scss b/client/src/standalone/videos/embed.scss
index 3631ea7e6..d15887478 100644
--- a/client/src/standalone/videos/embed.scss
+++ b/client/src/standalone/videos/embed.scss
@@ -24,7 +24,7 @@ html,
24body { 24body {
25 height: 100%; 25 height: 100%;
26 margin: 0; 26 margin: 0;
27 background-color: #000; 27 background-color: #0f0f10;
28} 28}
29 29
30#video-wrapper { 30#video-wrapper {
@@ -42,8 +42,10 @@ body {
42 } 42 }
43} 43}
44 44
45#error-block { 45#error-block,
46#video-password-block {
46 display: none; 47 display: none;
48 user-select: none;
47 49
48 flex-direction: column; 50 flex-direction: column;
49 align-content: center; 51 align-content: center;
@@ -86,6 +88,43 @@ body {
86 text-align: center; 88 text-align: center;
87} 89}
88 90
91#video-password-content {
92 @include margin(1rem, 0, 2rem);
93}
94
95#video-password-input,
96#video-password-submit {
97 line-height: 23px;
98 padding: 1rem;
99 margin: 1rem 0.5rem;
100 border: 0;
101 font-weight: 600;
102 border-radius: 3px!important;
103 font-size: 18px;
104 display: inline-block;
105}
106
107#video-password-submit {
108 color: #fff;
109 background-color: #f2690d;
110 cursor: pointer;
111}
112
113#video-password-submit:hover {
114 background-color: #f47825;
115}
116#video-password-error {
117 margin-top: 10px;
118 margin-bottom: 10px;
119 height: 2rem;
120 font-weight: bolder;
121}
122
123#video-password-block svg {
124 margin-left: auto;
125 margin-right: auto;
126}
127
89@media screen and (max-width: 300px) { 128@media screen and (max-width: 300px) {
90 #error-block { 129 #error-block {
91 font-size: 36px; 130 font-size: 36px;
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index cc4274b99..78b812ffd 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -1,18 +1,26 @@
1import './embed.scss' 1import './embed.scss'
2import '../../assets/player/shared/dock/peertube-dock-component' 2import '../../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 videojs from 'video.js' 5import videojs from 'video.js'
5import { peertubeTranslate } from '../../../../shared/core-utils/i18n' 6import {
6import { HTMLServerConfig, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement, VideoState } from '../../../../shared/models' 7 HTMLServerConfig,
7import { PeertubePlayerManager } from '../../assets/player' 8 ResultList,
9 ServerErrorCode,
10 VideoDetails,
11 VideoPlaylist,
12 VideoPlaylistElement,
13 VideoState
14} from '../../../../shared/models'
15import { PeerTubePlayer } from '../../assets/player/peertube-player'
8import { TranslationsManager } from '../../assets/player/translations-manager' 16import { TranslationsManager } from '../../assets/player/translations-manager'
9import { getParamString, logger, videoRequiresAuth } from '../../root-helpers' 17import { getParamString, logger, videoRequiresFileToken } from '../../root-helpers'
10import { PeerTubeEmbedApi } from './embed-api' 18import { PeerTubeEmbedApi } from './embed-api'
11import { 19import {
12 AuthHTTP, 20 AuthHTTP,
13 LiveManager, 21 LiveManager,
14 PeerTubePlugin, 22 PeerTubePlugin,
15 PlayerManagerOptions, 23 PlayerOptionsBuilder,
16 PlaylistFetcher, 24 PlaylistFetcher,
17 PlaylistTracker, 25 PlaylistTracker,
18 Translations, 26 Translations,
@@ -27,18 +35,26 @@ export class PeerTubeEmbed {
27 config: HTMLServerConfig 35 config: HTMLServerConfig
28 36
29 private translationsPromise: Promise<{ [id: string]: string }> 37 private translationsPromise: Promise<{ [id: string]: string }>
30 private PeertubePlayerManagerModulePromise: Promise<any> 38 private PeerTubePlayerManagerModulePromise: Promise<any>
31 39
32 private readonly http: AuthHTTP 40 private readonly http: AuthHTTP
33 private readonly videoFetcher: VideoFetcher 41 private readonly videoFetcher: VideoFetcher
34 private readonly playlistFetcher: PlaylistFetcher 42 private readonly playlistFetcher: PlaylistFetcher
35 private readonly peertubePlugin: PeerTubePlugin 43 private readonly peertubePlugin: PeerTubePlugin
36 private readonly playerHTML: PlayerHTML 44 private readonly playerHTML: PlayerHTML
37 private readonly playerManagerOptions: PlayerManagerOptions 45 private readonly playerOptionsBuilder: PlayerOptionsBuilder
38 private readonly liveManager: LiveManager 46 private readonly liveManager: LiveManager
39 47
48 private peertubePlayer: PeerTubePlayer
49
40 private playlistTracker: PlaylistTracker 50 private playlistTracker: PlaylistTracker
41 51
52 private alreadyInitialized = false
53 private alreadyPlayed = false
54
55 private videoPassword: string
56 private requiresPassword: boolean
57
42 constructor (videoWrapperId: string) { 58 constructor (videoWrapperId: string) {
43 logger.registerServerSending(window.location.origin) 59 logger.registerServerSending(window.location.origin)
44 60
@@ -48,8 +64,9 @@ export class PeerTubeEmbed {
48 this.playlistFetcher = new PlaylistFetcher(this.http) 64 this.playlistFetcher = new PlaylistFetcher(this.http)
49 this.peertubePlugin = new PeerTubePlugin(this.http) 65 this.peertubePlugin = new PeerTubePlugin(this.http)
50 this.playerHTML = new PlayerHTML(videoWrapperId) 66 this.playerHTML = new PlayerHTML(videoWrapperId)
51 this.playerManagerOptions = new PlayerManagerOptions(this.playerHTML, this.videoFetcher, this.peertubePlugin) 67 this.playerOptionsBuilder = new PlayerOptionsBuilder(this.playerHTML, this.videoFetcher, this.peertubePlugin)
52 this.liveManager = new LiveManager(this.playerHTML) 68 this.liveManager = new LiveManager(this.playerHTML)
69 this.requiresPassword = false
53 70
54 try { 71 try {
55 this.config = JSON.parse((window as any)['PeerTubeServerConfig']) 72 this.config = JSON.parse((window as any)['PeerTubeServerConfig'])
@@ -69,14 +86,14 @@ export class PeerTubeEmbed {
69 } 86 }
70 87
71 getScope () { 88 getScope () {
72 return this.playerManagerOptions.getScope() 89 return this.playerOptionsBuilder.getScope()
73 } 90 }
74 91
75 // --------------------------------------------------------------------------- 92 // ---------------------------------------------------------------------------
76 93
77 async init () { 94 async init () {
78 this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language) 95 this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language)
79 this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager') 96 this.PeerTubePlayerManagerModulePromise = import('../../assets/player/peertube-player')
80 97
81 // Issue when we parsed config from HTML, fallback to API 98 // Issue when we parsed config from HTML, fallback to API
82 if (!this.config) { 99 if (!this.config) {
@@ -90,7 +107,7 @@ export class PeerTubeEmbed {
90 107
91 if (!videoId) return 108 if (!videoId) return
92 109
93 return this.loadVideoAndBuildPlayer({ uuid: videoId, autoplayFromPreviousVideo: false, forceAutoplay: false }) 110 return this.loadVideoAndBuildPlayer({ uuid: videoId, forceAutoplay: false })
94 } 111 }
95 112
96 private async initPlaylist () { 113 private async initPlaylist () {
@@ -125,7 +142,7 @@ export class PeerTubeEmbed {
125 } 142 }
126 143
127 private initializeApi () { 144 private initializeApi () {
128 if (this.playerManagerOptions.hasAPIEnabled()) { 145 if (this.playerOptionsBuilder.hasAPIEnabled()) {
129 if (this.api) { 146 if (this.api) {
130 this.api.reInit() 147 this.api.reInit()
131 return 148 return
@@ -147,7 +164,7 @@ export class PeerTubeEmbed {
147 164
148 this.playlistTracker.setCurrentElement(next) 165 this.playlistTracker.setCurrentElement(next)
149 166
150 return this.loadVideoAndBuildPlayer({ uuid: next.video.uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }) 167 return this.loadVideoAndBuildPlayer({ uuid: next.video.uuid, forceAutoplay: false })
151 } 168 }
152 169
153 async playPreviousPlaylistVideo () { 170 async playPreviousPlaylistVideo () {
@@ -159,7 +176,7 @@ export class PeerTubeEmbed {
159 176
160 this.playlistTracker.setCurrentElement(previous) 177 this.playlistTracker.setCurrentElement(previous)
161 178
162 await this.loadVideoAndBuildPlayer({ uuid: previous.video.uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }) 179 await this.loadVideoAndBuildPlayer({ uuid: previous.video.uuid, forceAutoplay: false })
163 } 180 }
164 181
165 getCurrentPlaylistPosition () { 182 getCurrentPlaylistPosition () {
@@ -170,123 +187,124 @@ export class PeerTubeEmbed {
170 187
171 private async loadVideoAndBuildPlayer (options: { 188 private async loadVideoAndBuildPlayer (options: {
172 uuid: string 189 uuid: string
173 autoplayFromPreviousVideo: boolean
174 forceAutoplay: boolean 190 forceAutoplay: boolean
175 }) { 191 }) {
176 const { uuid, autoplayFromPreviousVideo, forceAutoplay } = options 192 const { uuid, forceAutoplay } = options
177 193
178 try { 194 try {
179 const { videoResponse, captionsPromise } = await this.videoFetcher.loadVideo(uuid) 195 const {
196 videoResponse,
197 captionsPromise,
198 storyboardsPromise
199 } = await this.videoFetcher.loadVideo({ videoId: uuid, videoPassword: this.videoPassword })
180 200
181 return this.buildVideoPlayer({ videoResponse, captionsPromise, autoplayFromPreviousVideo, forceAutoplay }) 201 return this.buildVideoPlayer({ videoResponse, captionsPromise, storyboardsPromise, forceAutoplay })
182 } catch (err) { 202 } catch (err) {
183 this.playerHTML.displayError(err.message, await this.translationsPromise) 203
204 if (await this.handlePasswordError(err)) this.loadVideoAndBuildPlayer({ ...options })
205 else this.playerHTML.displayError(err.message, await this.translationsPromise)
184 } 206 }
185 } 207 }
186 208
187 private async buildVideoPlayer (options: { 209 private async buildVideoPlayer (options: {
188 videoResponse: Response 210 videoResponse: Response
211 storyboardsPromise: Promise<Response>
189 captionsPromise: Promise<Response> 212 captionsPromise: Promise<Response>
190 autoplayFromPreviousVideo: boolean
191 forceAutoplay: boolean 213 forceAutoplay: boolean
192 }) { 214 }) {
193 const { videoResponse, captionsPromise, autoplayFromPreviousVideo, forceAutoplay } = options 215 const { videoResponse, captionsPromise, storyboardsPromise, forceAutoplay } = options
194
195 this.resetPlayerElement()
196 216
197 const videoInfoPromise = videoResponse.json() 217 const videoInfoPromise = videoResponse.json()
198 .then(async (videoInfo: VideoDetails) => { 218 .then(async (videoInfo: VideoDetails) => {
199 this.playerManagerOptions.loadParams(this.config, videoInfo) 219 this.playerOptionsBuilder.loadParams(this.config, videoInfo)
200 220
201 if (!autoplayFromPreviousVideo && !this.playerManagerOptions.hasAutoplay()) {
202 this.playerHTML.buildPlaceholder(videoInfo)
203 }
204 const live = videoInfo.isLive 221 const live = videoInfo.isLive
205 ? await this.videoFetcher.loadLive(videoInfo) 222 ? await this.videoFetcher.loadLive(videoInfo)
206 : undefined 223 : undefined
207 224
208 const videoFileToken = videoRequiresAuth(videoInfo) 225 const videoFileToken = videoRequiresFileToken(videoInfo)
209 ? await this.videoFetcher.loadVideoToken(videoInfo) 226 ? await this.videoFetcher.loadVideoToken(videoInfo, this.videoPassword)
210 : undefined 227 : undefined
211 228
212 return { live, video: videoInfo, videoFileToken } 229 return { live, video: videoInfo, videoFileToken }
213 }) 230 })
214 231
215 const [ { video, live, videoFileToken }, translations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([ 232 const [
233 { video, live, videoFileToken },
234 translations,
235 captionsResponse,
236 storyboardsResponse
237 ] = await Promise.all([
216 videoInfoPromise, 238 videoInfoPromise,
217 this.translationsPromise, 239 this.translationsPromise,
218 captionsPromise, 240 captionsPromise,
219 this.PeertubePlayerManagerModulePromise 241 storyboardsPromise,
242 this.buildPlayerIfNeeded()
220 ]) 243 ])
221 244
222 await this.peertubePlugin.loadPlugins(this.config, translations) 245 // If already played, we are in a playlist so we don't want to display the poster between videos
246 if (!this.alreadyPlayed) {
247 this.peertubePlayer.setPoster(window.location.origin + video.previewPath)
248 }
249
250 const playlist = this.playlistTracker
251 ? {
252 onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, forceAutoplay: false }),
223 253
224 const PlayerManager: typeof PeertubePlayerManager = PeertubePlayerManagerModule.PeertubePlayerManager 254 playlistTracker: this.playlistTracker,
255 playNext: () => this.playNextPlaylistVideo(),
256 playPrevious: () => this.playPreviousPlaylistVideo()
257 }
258 : undefined
225 259
226 const playerOptions = await this.playerManagerOptions.getPlayerOptions({ 260 const loadOptions = await this.playerOptionsBuilder.getPlayerLoadOptions({
227 video, 261 video,
228 captionsResponse, 262 captionsResponse,
229 autoplayFromPreviousVideo,
230 translations, 263 translations,
231 serverConfig: this.config,
232 264
233 authorizationHeader: () => this.http.getHeaderTokenValue(), 265 storyboardsResponse,
234 videoFileToken: () => videoFileToken,
235 266
236 onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }), 267 videoFileToken: () => videoFileToken,
268 videoPassword: () => this.videoPassword,
269 requiresPassword: this.requiresPassword,
237 270
238 playlistTracker: this.playlistTracker, 271 playlist,
239 playNextPlaylistVideo: () => this.playNextPlaylistVideo(),
240 playPreviousPlaylistVideo: () => this.playPreviousPlaylistVideo(),
241 272
242 live, 273 live,
243 forceAutoplay 274 forceAutoplay,
275 alreadyPlayed: this.alreadyPlayed
244 }) 276 })
277 await this.peertubePlayer.load(loadOptions)
245 278
246 this.player = await PlayerManager.initialize(this.playerManagerOptions.getMode(), playerOptions, (player: videojs.Player) => { 279 if (!this.alreadyInitialized) {
247 this.player = player 280 this.player = this.peertubePlayer.getPlayer();
248 })
249 281
250 this.player.on('customError', (event: any, data: any) => { 282 (window as any)['videojsPlayer'] = this.player
251 const message = data?.err?.message || ''
252 if (!message.includes('from xs param')) return
253 283
254 this.player.dispose() 284 this.buildCSS()
255 this.playerHTML.removePlayerElement() 285 this.initializeApi()
256 this.playerHTML.displayError('This video is not available because the remote instance is not responding.', translations) 286 }
257 });
258
259 (window as any)['videojsPlayer'] = this.player
260
261 this.buildCSS()
262 this.buildPlayerDock(video)
263 this.initializeApi()
264 287
265 this.playerHTML.removePlaceholder() 288 this.alreadyInitialized = true
266 289
267 if (this.isPlaylistEmbed()) { 290 this.player.one('play', () => {
268 await this.buildPlayerPlaylistUpnext() 291 this.alreadyPlayed = true
292 })
269 293
270 this.player.playlist().updateSelected() 294 if (this.videoPassword) this.playerHTML.removeVideoPasswordBlock()
271
272 this.player.on('stopped', () => {
273 this.playNextPlaylistVideo()
274 })
275 }
276 295
277 if (video.isLive) { 296 if (video.isLive) {
278 this.liveManager.listenForChanges({ 297 this.liveManager.listenForChanges({
279 video, 298 video,
280 onPublishedVideo: () => { 299 onPublishedVideo: () => {
281 this.liveManager.stopListeningForChanges(video) 300 this.liveManager.stopListeningForChanges(video)
282 this.loadVideoAndBuildPlayer({ uuid: video.uuid, autoplayFromPreviousVideo: false, forceAutoplay: true }) 301 this.loadVideoAndBuildPlayer({ uuid: video.uuid, forceAutoplay: true })
283 } 302 }
284 }) 303 })
285 304
286 if (video.state.id === VideoState.WAITING_FOR_LIVE || video.state.id === VideoState.LIVE_ENDED) { 305 if (video.state.id === VideoState.WAITING_FOR_LIVE || video.state.id === VideoState.LIVE_ENDED) {
287 this.liveManager.displayInfo({ state: video.state.id, translations }) 306 this.liveManager.displayInfo({ state: video.state.id, translations })
288 307 this.peertubePlayer.disable()
289 this.disablePlayer()
290 } else { 308 } else {
291 this.correctlyHandleLiveEnding(translations) 309 this.correctlyHandleLiveEnding(translations)
292 } 310 }
@@ -295,74 +313,15 @@ export class PeerTubeEmbed {
295 this.peertubePlugin.getPluginsManager().runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video }) 313 this.peertubePlugin.getPluginsManager().runHook('action:embed.player.loaded', undefined, { player: this.player, videojs, video })
296 } 314 }
297 315
298 private resetPlayerElement () {
299 if (this.player) {
300 this.player.dispose()
301 this.player = undefined
302 }
303
304 const playerElement = document.createElement('video')
305 playerElement.className = 'video-js vjs-peertube-skin'
306 playerElement.setAttribute('playsinline', 'true')
307
308 this.playerHTML.setPlayerElement(playerElement)
309 this.playerHTML.addPlayerElementToDOM()
310 }
311
312 private async buildPlayerPlaylistUpnext () {
313 const translations = await this.translationsPromise
314
315 this.player.upnext({
316 timeout: 10000, // 10s
317 headText: peertubeTranslate('Up Next', translations),
318 cancelText: peertubeTranslate('Cancel', translations),
319 suspendedText: peertubeTranslate('Autoplay is suspended', translations),
320 getTitle: () => this.playlistTracker.nextVideoTitle(),
321 next: () => this.playNextPlaylistVideo(),
322 condition: () => !!this.playlistTracker.getNextPlaylistElement(),
323 suspended: () => false
324 })
325 }
326
327 private buildPlayerDock (videoInfo: VideoDetails) {
328 if (!this.playerManagerOptions.hasControls()) return
329
330 // On webtorrent fallback, player may have been disposed
331 if (!this.player.player_) return
332
333 const title = this.playerManagerOptions.hasTitle()
334 ? videoInfo.name
335 : undefined
336
337 const description = this.playerManagerOptions.hasWarningTitle() && this.playerManagerOptions.hasP2PEnabled()
338 ? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>'
339 : undefined
340
341 if (!title && !description) return
342
343 const availableAvatars = videoInfo.channel.avatars.filter(a => a.width < 50)
344 const avatar = availableAvatars.length !== 0
345 ? availableAvatars[0]
346 : undefined
347
348 this.player.peertubeDock({
349 title,
350 description,
351 avatarUrl: title && avatar
352 ? avatar.path
353 : undefined
354 })
355 }
356
357 private buildCSS () { 316 private buildCSS () {
358 const body = document.getElementById('custom-css') 317 const body = document.getElementById('custom-css')
359 318
360 if (this.playerManagerOptions.hasBigPlayBackgroundColor()) { 319 if (this.playerOptionsBuilder.hasBigPlayBackgroundColor()) {
361 body.style.setProperty('--embedBigPlayBackgroundColor', this.playerManagerOptions.getBigPlayBackgroundColor()) 320 body.style.setProperty('--embedBigPlayBackgroundColor', this.playerOptionsBuilder.getBigPlayBackgroundColor())
362 } 321 }
363 322
364 if (this.playerManagerOptions.hasForegroundColor()) { 323 if (this.playerOptionsBuilder.hasForegroundColor()) {
365 body.style.setProperty('--embedForegroundColor', this.playerManagerOptions.getForegroundColor()) 324 body.style.setProperty('--embedForegroundColor', this.playerOptionsBuilder.getForegroundColor())
366 } 325 }
367 } 326 }
368 327
@@ -384,23 +343,52 @@ export class PeerTubeEmbed {
384 // Display the live ended information 343 // Display the live ended information
385 this.liveManager.displayInfo({ state: VideoState.LIVE_ENDED, translations }) 344 this.liveManager.displayInfo({ state: VideoState.LIVE_ENDED, translations })
386 345
387 this.disablePlayer() 346 this.peertubePlayer.disable()
388 }) 347 })
389 } 348 }
390 349
391 private disablePlayer () { 350 private async handlePasswordError (err: PeerTubeServerError) {
392 if (this.player.isFullscreen()) { 351 let incorrectPassword: boolean = null
393 this.player.exitFullscreen() 352 if (err.serverCode === ServerErrorCode.VIDEO_REQUIRES_PASSWORD) incorrectPassword = false
394 } 353 else if (err.serverCode === ServerErrorCode.INCORRECT_VIDEO_PASSWORD) incorrectPassword = true
395 354
396 // Disable player 355 if (incorrectPassword === null) return false
397 this.player.hasStarted(false)
398 this.player.removeClass('vjs-has-autoplay')
399 this.player.bigPlayButton.hide();
400 356
401 (this.player.el() as HTMLElement).style.pointerEvents = 'none' 357 this.requiresPassword = true
358 this.videoPassword = await this.playerHTML.askVideoPassword({
359 incorrectPassword,
360 translations: await this.translationsPromise
361 })
362 return true
402 } 363 }
403 364
365 private async buildPlayerIfNeeded () {
366 if (this.peertubePlayer) {
367 this.peertubePlayer.enable()
368
369 return
370 }
371
372 const playerElement = document.createElement('video')
373 playerElement.className = 'video-js vjs-peertube-skin'
374 playerElement.setAttribute('playsinline', 'true')
375
376 this.playerHTML.setPlayerElement(playerElement)
377 this.playerHTML.addPlayerElementToDOM()
378
379 const [ { PeerTubePlayer } ] = await Promise.all([
380 this.PeerTubePlayerManagerModulePromise,
381 this.peertubePlugin.loadPlugins(this.config, await this.translationsPromise)
382 ])
383
384 const constructorOptions = this.playerOptionsBuilder.getPlayerConstructorOptions({
385 serverConfig: this.config,
386 authorizationHeader: () => this.http.getHeaderTokenValue()
387 })
388 this.peertubePlayer = new PeerTubePlayer(constructorOptions)
389
390 this.player = this.peertubePlayer.getPlayer()
391 }
404} 392}
405 393
406PeerTubeEmbed.main() 394PeerTubeEmbed.main()
diff --git a/client/src/standalone/videos/shared/auth-http.ts b/client/src/standalone/videos/shared/auth-http.ts
index 95e3b029e..c1e9f7750 100644
--- a/client/src/standalone/videos/shared/auth-http.ts
+++ b/client/src/standalone/videos/shared/auth-http.ts
@@ -18,10 +18,12 @@ export class AuthHTTP {
18 if (this.userOAuthTokens) this.setHeadersFromTokens() 18 if (this.userOAuthTokens) this.setHeadersFromTokens()
19 } 19 }
20 20
21 fetch (url: string, { optionalAuth, method }: { optionalAuth: boolean, method?: string }) { 21 fetch (url: string, { optionalAuth, method }: { optionalAuth: boolean, method?: string }, videoPassword?: string) {
22 const refreshFetchOptions = optionalAuth 22 let refreshFetchOptions: { headers?: Headers } = {}
23 ? { headers: this.headers } 23
24 : {} 24 if (videoPassword) this.headers.set('x-peertube-video-password', videoPassword)
25
26 if (videoPassword || optionalAuth) refreshFetchOptions = { headers: this.headers }
25 27
26 return this.refreshFetch(url.toString(), { ...refreshFetchOptions, method }) 28 return this.refreshFetch(url.toString(), { ...refreshFetchOptions, method })
27 } 29 }
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 d93678c10..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
@@ -55,17 +54,55 @@ export class PlayerHTML {
55 this.wrapperElement.style.display = 'none' 54 this.wrapperElement.style.display = 'none'
56 } 55 }
57 56
58 buildPlaceholder (video: VideoDetails) { 57 async askVideoPassword (options: { incorrectPassword: boolean, translations: Translations }): Promise<string> {
59 const placeholder = this.getPlaceholderElement() 58 const { incorrectPassword, translations } = options
59 return new Promise((resolve) => {
60 60
61 const url = window.location.origin + video.previewPath 61 this.wrapperElement.style.display = 'none'
62 placeholder.style.backgroundImage = `url("${url}")` 62
63 placeholder.style.display = 'block' 63 const translatedTitle = peertubeTranslate('This video is password protected', translations)
64 const translatedMessage = peertubeTranslate('You need a password to watch this video.', translations)
65
66 document.title = translatedTitle
67
68 const videoPasswordBlock = document.getElementById('video-password-block')
69 videoPasswordBlock.style.display = 'flex'
70
71 const videoPasswordTitle = document.getElementById('video-password-title')
72 videoPasswordTitle.innerHTML = translatedTitle
73
74 const videoPasswordMessage = document.getElementById('video-password-content')
75 videoPasswordMessage.innerHTML = translatedMessage
76
77 if (incorrectPassword) {
78 const videoPasswordError = document.getElementById('video-password-error')
79 videoPasswordError.innerHTML = peertubeTranslate('Incorrect password, please enter a correct password', translations)
80 videoPasswordError.style.transform = 'scale(1.2)'
81
82 setTimeout(() => {
83 videoPasswordError.style.transform = 'scale(1)'
84 }, 500)
85 }
86
87 const videoPasswordSubmitButton = document.getElementById('video-password-submit')
88 videoPasswordSubmitButton.innerHTML = peertubeTranslate('Watch Video', translations)
89
90 const videoPasswordInput = document.getElementById('video-password-input') as HTMLInputElement
91 videoPasswordInput.placeholder = peertubeTranslate('Password', translations)
92
93 const videoPasswordForm = document.getElementById('video-password-form')
94 videoPasswordForm.addEventListener('submit', (event) => {
95 event.preventDefault()
96 const videoPassword = videoPasswordInput.value
97 resolve(videoPassword)
98 })
99 })
64 } 100 }
65 101
66 removePlaceholder () { 102 removeVideoPasswordBlock () {
67 const placeholder = this.getPlaceholderElement() 103 const videoPasswordBlock = document.getElementById('video-password-block')
68 placeholder.style.display = 'none' 104 videoPasswordBlock.style.display = 'none'
105 this.wrapperElement.style.display = 'block'
69 } 106 }
70 107
71 displayInformation (text: string, translations: Translations) { 108 displayInformation (text: string, translations: Translations) {
@@ -85,10 +122,6 @@ export class PlayerHTML {
85 this.informationElement = undefined 122 this.informationElement = undefined
86 } 123 }
87 124
88 private getPlaceholderElement () {
89 return document.getElementById('placeholder-preview')
90 }
91
92 private removeElement (element: HTMLElement) { 125 private removeElement (element: HTMLElement) {
93 element.parentElement.removeChild(element) 126 element.parentElement.removeChild(element)
94 } 127 }
diff --git a/client/src/standalone/videos/shared/player-manager-options.ts b/client/src/standalone/videos/shared/player-options-builder.ts
index 43ae22a3b..8a4e32444 100644
--- a/client/src/standalone/videos/shared/player-manager-options.ts
+++ b/client/src/standalone/videos/shared/player-options-builder.ts
@@ -2,6 +2,7 @@ import { peertubeTranslate } from '../../../../../shared/core-utils/i18n'
2import { 2import {
3 HTMLServerConfig, 3 HTMLServerConfig,
4 LiveVideo, 4 LiveVideo,
5 Storyboard,
5 Video, 6 Video,
6 VideoCaption, 7 VideoCaption,
7 VideoDetails, 8 VideoDetails,
@@ -9,7 +10,7 @@ import {
9 VideoState, 10 VideoState,
10 VideoStreamingPlaylistType 11 VideoStreamingPlaylistType
11} from '../../../../../shared/models' 12} from '../../../../../shared/models'
12import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode, VideoJSCaption } from '../../../assets/player' 13import { HLSOptions, PeerTubePlayerContructorOptions, PeerTubePlayerLoadOptions, PlayerMode, VideoJSCaption } from '../../../assets/player'
13import { 14import {
14 getBoolOrDefault, 15 getBoolOrDefault,
15 getParamString, 16 getParamString,
@@ -18,7 +19,7 @@ import {
18 logger, 19 logger,
19 peertubeLocalStorage, 20 peertubeLocalStorage,
20 UserLocalStorageKeys, 21 UserLocalStorageKeys,
21 videoRequiresAuth 22 videoRequiresUserAuth
22} from '../../../root-helpers' 23} from '../../../root-helpers'
23import { PeerTubePlugin } from './peertube-plugin' 24import { PeerTubePlugin } from './peertube-plugin'
24import { PlayerHTML } from './player-html' 25import { PlayerHTML } from './player-html'
@@ -26,7 +27,7 @@ import { PlaylistTracker } from './playlist-tracker'
26import { Translations } from './translations' 27import { Translations } from './translations'
27import { VideoFetcher } from './video-fetcher' 28import { VideoFetcher } from './video-fetcher'
28 29
29export class PlayerManagerOptions { 30export class PlayerOptionsBuilder {
30 private autoplay: boolean 31 private autoplay: boolean
31 32
32 private controls: boolean 33 private controls: boolean
@@ -140,10 +141,10 @@ export class PlayerManagerOptions {
140 141
141 if (modeParam) { 142 if (modeParam) {
142 if (modeParam === 'p2p-media-loader') this.mode = 'p2p-media-loader' 143 if (modeParam === 'p2p-media-loader') this.mode = 'p2p-media-loader'
143 else this.mode = 'webtorrent' 144 else this.mode = 'web-video'
144 } else { 145 } else {
145 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'
146 else this.mode = 'webtorrent' 147 else this.mode = 'web-video'
147 } 148 }
148 } catch (err) { 149 } catch (err) {
149 logger.error('Cannot get params from URL.', err) 150 logger.error('Cannot get params from URL.', err)
@@ -152,119 +153,140 @@ export class PlayerManagerOptions {
152 153
153 // --------------------------------------------------------------------------- 154 // ---------------------------------------------------------------------------
154 155
155 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: {
156 video: VideoDetails 197 video: VideoDetails
157 captionsResponse: Response 198 captionsResponse: Response
199
200 storyboardsResponse: Response
201
158 live?: LiveVideo 202 live?: LiveVideo
159 203
204 alreadyPlayed: boolean
160 forceAutoplay: boolean 205 forceAutoplay: boolean
161 206
162 authorizationHeader: () => string
163 videoFileToken: () => string 207 videoFileToken: () => string
164 208
165 serverConfig: HTMLServerConfig 209 videoPassword: () => string
166 210 requiresPassword: boolean
167 autoplayFromPreviousVideo: boolean
168 211
169 translations: Translations 212 translations: Translations
170 213
171 playlistTracker?: PlaylistTracker 214 playlist?: {
172 playNextPlaylistVideo?: () => any 215 playlistTracker: PlaylistTracker
173 playPreviousPlaylistVideo?: () => any 216 playNext: () => any
174 onVideoUpdate?: (uuid: string) => any 217 playPrevious: () => any
175 }) { 218 onVideoUpdate: (uuid: string) => any
219 }
220 }): Promise<PeerTubePlayerLoadOptions> {
176 const { 221 const {
177 video, 222 video,
178 captionsResponse, 223 captionsResponse,
179 autoplayFromPreviousVideo,
180 videoFileToken, 224 videoFileToken,
225 videoPassword,
226 requiresPassword,
181 translations, 227 translations,
228 alreadyPlayed,
182 forceAutoplay, 229 forceAutoplay,
183 playlistTracker, 230 playlist,
184 live, 231 live,
185 authorizationHeader, 232 storyboardsResponse
186 serverConfig
187 } = options 233 } = options
188 234
189 const videoCaptions = await this.buildCaptions(captionsResponse, translations) 235 const [ videoCaptions, storyboard ] = await Promise.all([
190 236 this.buildCaptions(captionsResponse, translations),
191 const playerOptions: PeertubePlayerManagerOptions = { 237 this.buildStoryboard(storyboardsResponse)
192 common: { 238 ])
193 // Autoplay in playlist mode
194 autoplay: autoplayFromPreviousVideo ? true : this.autoplay,
195 forceAutoplay,
196 239
197 controls: this.controls, 240 return {
198 controlBar: this.controlBar, 241 mode: this.mode,
199
200 muted: this.muted,
201 loop: this.loop,
202 242
203 p2pEnabled: this.p2pEnabled, 243 autoplay: forceAutoplay || alreadyPlayed || this.autoplay,
244 forceAutoplay,
204 245
205 captions: videoCaptions.length !== 0, 246 p2pEnabled: this.p2pEnabled,
206 subtitle: this.subtitle,
207 247
208 startTime: playlistTracker 248 subtitle: this.subtitle,
209 ? playlistTracker.getCurrentElement().startTimestamp
210 : this.startTime,
211 stopTime: playlistTracker
212 ? playlistTracker.getCurrentElement().stopTimestamp
213 : this.stopTime,
214 249
215 playbackRate: this.playbackRate, 250 storyboard,
216 251
217 videoCaptions, 252 startTime: playlist
218 inactivityTimeout: 2500, 253 ? playlist.playlistTracker.getCurrentElement().startTimestamp
219 videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid), 254 : this.startTime,
220 videoViewIntervalMs: 5000, 255 stopTime: playlist
221 metricsUrl: window.location.origin + '/api/v1/metrics/playback', 256 ? playlist.playlistTracker.getCurrentElement().stopTimestamp
257 : this.stopTime,
222 258
223 videoShortUUID: video.shortUUID, 259 videoCaptions,
224 videoUUID: video.uuid, 260 videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid),
225 261
226 playerElement: this.playerHTML.getPlayerElement(), 262 videoShortUUID: video.shortUUID,
227 onPlayerElementChange: (element: HTMLVideoElement) => { 263 videoUUID: video.uuid,
228 this.playerHTML.setPlayerElement(element)
229 },
230 264
231 videoDuration: video.duration, 265 duration: video.duration,
232 enableHotkeys: true,
233 266
234 peertubeLink: this.peertubeLink, 267 poster: window.location.origin + video.previewPath,
235 instanceName: serverConfig.instance.name,
236 268
237 poster: window.location.origin + video.previewPath, 269 embedUrl: window.location.origin + video.embedPath,
238 theaterButton: false, 270 embedTitle: video.name,
239 271
240 serverUrl: window.location.origin, 272 requiresUserAuth: videoRequiresUserAuth(video),
241 language: navigator.language, 273 videoFileToken,
242 embedUrl: window.location.origin + video.embedPath,
243 embedTitle: video.name,
244 274
245 requiresAuth: videoRequiresAuth(video), 275 requiresPassword,
246 authorizationHeader, 276 videoPassword,
247 videoFileToken,
248 277
249 errorNotifier: () => { 278 ...this.buildLiveOptions(video, live),
250 // Empty, we don't have a notifier in the embed
251 },
252 279
253 ...this.buildLiveOptions(video, live), 280 ...this.buildPlaylistOptions(playlist),
254 281
255 ...this.buildPlaylistOptions(options) 282 dock: this.buildDockOptions(video),
256 },
257 283
258 webtorrent: { 284 webVideo: {
259 videoFiles: video.files 285 videoFiles: video.files
260 }, 286 },
261 287
262 ...this.buildP2PMediaLoaderOptions(video), 288 hls: this.buildHLSOptions(video)
263
264 pluginsManager: this.peertubePlugin.getPluginsManager()
265 } 289 }
266
267 return playerOptions
268 } 290 }
269 291
270 private buildLiveOptions (video: VideoDetails, live: LiveVideo) { 292 private buildLiveOptions (video: VideoDetails, live: LiveVideo) {
@@ -278,15 +300,39 @@ export class PlayerManagerOptions {
278 } 300 }
279 } 301 }
280 302
281 private buildPlaylistOptions (options: { 303 private async buildStoryboard (storyboardsResponse: Response) {
282 playlistTracker?: PlaylistTracker 304 const { storyboards } = await storyboardsResponse.json() as { storyboards: Storyboard[] }
283 playNextPlaylistVideo?: () => any 305 if (!storyboards || storyboards.length === 0) return undefined
284 playPreviousPlaylistVideo?: () => any 306
285 onVideoUpdate?: (uuid: string) => any 307 return {
308 url: window.location.origin + storyboards[0].storyboardPath,
309 height: storyboards[0].spriteHeight,
310 width: storyboards[0].spriteWidth,
311 interval: storyboards[0].spriteDuration
312 }
313 }
314
315 private buildPlaylistOptions (options?: {
316 playlistTracker: PlaylistTracker
317 playNext: () => any
318 playPrevious: () => any
319 onVideoUpdate: (uuid: string) => any
286 }) { 320 }) {
287 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 }
288 334
289 if (!playlistTracker) return {} 335 const { playlistTracker, playNext, playPrevious, onVideoUpdate } = options
290 336
291 return { 337 return {
292 playlist: { 338 playlist: {
@@ -302,27 +348,37 @@ export class PlayerManagerOptions {
302 } 348 }
303 }, 349 },
304 350
305 nextVideo: () => playNextPlaylistVideo(), 351 previousVideo: {
306 hasNextVideo: () => playlistTracker.hasNextPlaylistElement(), 352 enabled: playlistTracker.hasPreviousPlaylistElement(),
353 handler: () => playPrevious(),
354 displayControlBarButton: true
355 },
307 356
308 previousVideo: () => playPreviousPlaylistVideo(), 357 nextVideo: {
309 hasPreviousVideo: () => playlistTracker.hasPreviousPlaylistElement() 358 enabled: playlistTracker.hasNextPlaylistElement(),
359 handler: () => playNext(),
360 getVideoTitle: () => playlistTracker.getNextPlaylistElement()?.video?.name,
361 displayControlBarButton: true
362 },
363
364 upnext: {
365 isEnabled: () => true,
366 isSuspended: () => false,
367 timeout: 0
368 }
310 } 369 }
311 } 370 }
312 371
313 private buildP2PMediaLoaderOptions (video: VideoDetails) { 372 private buildHLSOptions (video: VideoDetails): HLSOptions {
314 if (this.mode !== 'p2p-media-loader') return {}
315
316 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
317 375
318 return { 376 return {
319 p2pMediaLoader: { 377 playlistUrl: hlsPlaylist.playlistUrl,
320 playlistUrl: hlsPlaylist.playlistUrl, 378 segmentsSha256Url: hlsPlaylist.segmentsSha256Url,
321 segmentsSha256Url: hlsPlaylist.segmentsSha256Url, 379 redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl),
322 redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), 380 trackerAnnounce: video.trackerUrls,
323 trackerAnnounce: video.trackerUrls, 381 videoFiles: hlsPlaylist.files
324 videoFiles: hlsPlaylist.files
325 } as P2PMediaLoaderOptions
326 } 382 }
327 } 383 }
328 384
@@ -344,6 +400,35 @@ export class PlayerManagerOptions {
344 400
345 // --------------------------------------------------------------------------- 401 // ---------------------------------------------------------------------------
346 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
347 private isP2PEnabled (config: HTMLServerConfig, video: Video) { 432 private isP2PEnabled (config: HTMLServerConfig, video: Video) {
348 const userP2PEnabled = getBoolOrDefault( 433 const userP2PEnabled = getBoolOrDefault(
349 peertubeLocalStorage.getItem(UserLocalStorageKeys.P2P_ENABLED), 434 peertubeLocalStorage.getItem(UserLocalStorageKeys.P2P_ENABLED),
diff --git a/client/src/standalone/videos/shared/video-fetcher.ts b/client/src/standalone/videos/shared/video-fetcher.ts
index cf6d12831..7fb94fbf3 100644
--- a/client/src/standalone/videos/shared/video-fetcher.ts
+++ b/client/src/standalone/videos/shared/video-fetcher.ts
@@ -1,5 +1,6 @@
1import { HttpStatusCode, LiveVideo, VideoDetails, VideoToken } from '../../../../../shared/models' 1import { HttpStatusCode, LiveVideo, VideoDetails, VideoToken } from '../../../../../shared/models'
2import { logger } from '../../../root-helpers' 2import { logger } from '../../../root-helpers'
3import { PeerTubeServerError } from '../../../types'
3import { AuthHTTP } from './auth-http' 4import { AuthHTTP } from './auth-http'
4 5
5export class VideoFetcher { 6export class VideoFetcher {
@@ -8,8 +9,8 @@ export class VideoFetcher {
8 9
9 } 10 }
10 11
11 async loadVideo (videoId: string) { 12 async loadVideo ({ videoId, videoPassword }: { videoId: string, videoPassword?: string }) {
12 const videoPromise = this.loadVideoInfo(videoId) 13 const videoPromise = this.loadVideoInfo({ videoId, videoPassword })
13 14
14 let videoResponse: Response 15 let videoResponse: Response
15 let isResponseOk: boolean 16 let isResponseOk: boolean
@@ -27,13 +28,17 @@ export class VideoFetcher {
27 if (videoResponse?.status === HttpStatusCode.NOT_FOUND_404) { 28 if (videoResponse?.status === HttpStatusCode.NOT_FOUND_404) {
28 throw new Error('This video does not exist.') 29 throw new Error('This video does not exist.')
29 } 30 }
30 31 if (videoResponse?.status === HttpStatusCode.FORBIDDEN_403) {
32 const res = await videoResponse.json()
33 throw new PeerTubeServerError(res.message, res.code)
34 }
31 throw new Error('We cannot fetch the video. Please try again later.') 35 throw new Error('We cannot fetch the video. Please try again later.')
32 } 36 }
33 37
34 const captionsPromise = this.loadVideoCaptions(videoId) 38 const captionsPromise = this.loadVideoCaptions({ videoId, videoPassword })
39 const storyboardsPromise = this.loadStoryboards(videoId)
35 40
36 return { captionsPromise, videoResponse } 41 return { captionsPromise, storyboardsPromise, videoResponse }
37 } 42 }
38 43
39 loadLive (video: VideoDetails) { 44 loadLive (video: VideoDetails) {
@@ -41,8 +46,8 @@ export class VideoFetcher {
41 .then(res => res.json() as Promise<LiveVideo>) 46 .then(res => res.json() as Promise<LiveVideo>)
42 } 47 }
43 48
44 loadVideoToken (video: VideoDetails) { 49 loadVideoToken (video: VideoDetails, videoPassword?: string) {
45 return this.http.fetch(this.getVideoTokenUrl(video.uuid), { optionalAuth: true, method: 'POST' }) 50 return this.http.fetch(this.getVideoTokenUrl(video.uuid), { optionalAuth: true, method: 'POST' }, videoPassword)
46 .then(res => res.json() as Promise<VideoToken>) 51 .then(res => res.json() as Promise<VideoToken>)
47 .then(token => token.files.token) 52 .then(token => token.files.token)
48 } 53 }
@@ -51,12 +56,12 @@ export class VideoFetcher {
51 return this.getVideoUrl(videoUUID) + '/views' 56 return this.getVideoUrl(videoUUID) + '/views'
52 } 57 }
53 58
54 private loadVideoInfo (videoId: string): Promise<Response> { 59 private loadVideoInfo ({ videoId, videoPassword }: { videoId: string, videoPassword?: string }): Promise<Response> {
55 return this.http.fetch(this.getVideoUrl(videoId), { optionalAuth: true }) 60 return this.http.fetch(this.getVideoUrl(videoId), { optionalAuth: true }, videoPassword)
56 } 61 }
57 62
58 private loadVideoCaptions (videoId: string): Promise<Response> { 63 private loadVideoCaptions ({ videoId, videoPassword }: { videoId: string, videoPassword?: string }): Promise<Response> {
59 return this.http.fetch(this.getVideoUrl(videoId) + '/captions', { optionalAuth: true }) 64 return this.http.fetch(this.getVideoUrl(videoId) + '/captions', { optionalAuth: true }, videoPassword)
60 } 65 }
61 66
62 private getVideoUrl (id: string) { 67 private getVideoUrl (id: string) {
@@ -67,6 +72,14 @@ export class VideoFetcher {
67 return window.location.origin + '/api/v1/videos/live/' + videoId 72 return window.location.origin + '/api/v1/videos/live/' + videoId
68 } 73 }
69 74
75 private loadStoryboards (videoUUID: string): Promise<Response> {
76 return this.http.fetch(this.getStoryboardsUrl(videoUUID), { optionalAuth: true })
77 }
78
79 private getStoryboardsUrl (videoId: string) {
80 return window.location.origin + '/api/v1/videos/' + videoId + '/storyboards'
81 }
82
70 private getVideoTokenUrl (id: string) { 83 private getVideoTokenUrl (id: string) {
71 return this.getVideoUrl(id) + '/token' 84 return this.getVideoUrl(id) + '/token'
72 } 85 }
diff --git a/client/src/standalone/videos/test-embed.ts b/client/src/standalone/videos/test-embed.ts
index b34df11ee..b7a283c4d 100644
--- a/client/src/standalone/videos/test-embed.ts
+++ b/client/src/standalone/videos/test-embed.ts
@@ -1,6 +1,6 @@
1import './test-embed.scss' 1import './test-embed.scss'
2import { PeerTubeResolution, PlayerEventType } from '../player/definitions' 2import { PeerTubeResolution, PlayerEventType } from '../embed-player-api/definitions'
3import { PeerTubePlayer } from '../player/player' 3import { PeerTubePlayer } from '../embed-player-api/player'
4import { logger } from '../../root-helpers' 4import { logger } from '../../root-helpers'
5 5
6window.addEventListener('load', async () => { 6window.addEventListener('load', async () => {