aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/standalone
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/standalone')
-rw-r--r--client/src/standalone/videos/embed.html17
-rw-r--r--client/src/standalone/videos/embed.scss43
-rw-r--r--client/src/standalone/videos/embed.ts44
-rw-r--r--client/src/standalone/videos/shared/auth-http.ts10
-rw-r--r--client/src/standalone/videos/shared/player-html.ts52
-rw-r--r--client/src/standalone/videos/shared/player-manager-options.ts12
-rw-r--r--client/src/standalone/videos/shared/video-fetcher.ts24
7 files changed, 178 insertions, 24 deletions
diff --git a/client/src/standalone/videos/embed.html b/client/src/standalone/videos/embed.html
index 32bf5f655..a74bb4cee 100644
--- a/client/src/standalone/videos/embed.html
+++ b/client/src/standalone/videos/embed.html
@@ -41,6 +41,23 @@
41 <div id="error-content"></div> 41 <div id="error-content"></div>
42 </div> 42 </div>
43 43
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" required>
52 <button type="submit" id="video-password-submit"> </button>
53 </form>
54
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
44 <div id="video-wrapper"></div> 61 <div id="video-wrapper"></div>
45 62
46 <div id="placeholder-preview"></div> 63 <div id="placeholder-preview"></div>
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..cffda2cc7 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -3,10 +3,18 @@ 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 videojs from 'video.js' 4import videojs from 'video.js'
5import { peertubeTranslate } from '../../../../shared/core-utils/i18n' 5import { peertubeTranslate } from '../../../../shared/core-utils/i18n'
6import { HTMLServerConfig, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement, VideoState } from '../../../../shared/models' 6import {
7 HTMLServerConfig,
8 ResultList,
9 ServerErrorCode,
10 VideoDetails,
11 VideoPlaylist,
12 VideoPlaylistElement,
13 VideoState
14} from '../../../../shared/models'
7import { PeertubePlayerManager } from '../../assets/player' 15import { PeertubePlayerManager } from '../../assets/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,
@@ -19,6 +27,7 @@ import {
19 VideoFetcher 27 VideoFetcher
20} from './shared' 28} from './shared'
21import { PlayerHTML } from './shared/player-html' 29import { PlayerHTML } from './shared/player-html'
30import { PeerTubeServerError } from 'src/types'
22 31
23export class PeerTubeEmbed { 32export class PeerTubeEmbed {
24 player: videojs.Player 33 player: videojs.Player
@@ -38,6 +47,8 @@ export class PeerTubeEmbed {
38 private readonly liveManager: LiveManager 47 private readonly liveManager: LiveManager
39 48
40 private playlistTracker: PlaylistTracker 49 private playlistTracker: PlaylistTracker
50 private videoPassword: string
51 private requiresPassword: boolean
41 52
42 constructor (videoWrapperId: string) { 53 constructor (videoWrapperId: string) {
43 logger.registerServerSending(window.location.origin) 54 logger.registerServerSending(window.location.origin)
@@ -50,6 +61,7 @@ export class PeerTubeEmbed {
50 this.playerHTML = new PlayerHTML(videoWrapperId) 61 this.playerHTML = new PlayerHTML(videoWrapperId)
51 this.playerManagerOptions = new PlayerManagerOptions(this.playerHTML, this.videoFetcher, this.peertubePlugin) 62 this.playerManagerOptions = new PlayerManagerOptions(this.playerHTML, this.videoFetcher, this.peertubePlugin)
52 this.liveManager = new LiveManager(this.playerHTML) 63 this.liveManager = new LiveManager(this.playerHTML)
64 this.requiresPassword = false
53 65
54 try { 66 try {
55 this.config = JSON.parse((window as any)['PeerTubeServerConfig']) 67 this.config = JSON.parse((window as any)['PeerTubeServerConfig'])
@@ -176,11 +188,13 @@ export class PeerTubeEmbed {
176 const { uuid, autoplayFromPreviousVideo, forceAutoplay } = options 188 const { uuid, autoplayFromPreviousVideo, forceAutoplay } = options
177 189
178 try { 190 try {
179 const { videoResponse, captionsPromise } = await this.videoFetcher.loadVideo(uuid) 191 const { videoResponse, captionsPromise } = await this.videoFetcher.loadVideo({ videoId: uuid, videoPassword: this.videoPassword })
180 192
181 return this.buildVideoPlayer({ videoResponse, captionsPromise, autoplayFromPreviousVideo, forceAutoplay }) 193 return this.buildVideoPlayer({ videoResponse, captionsPromise, autoplayFromPreviousVideo, forceAutoplay })
182 } catch (err) { 194 } catch (err) {
183 this.playerHTML.displayError(err.message, await this.translationsPromise) 195
196 if (await this.handlePasswordError(err)) this.loadVideoAndBuildPlayer({ ...options })
197 else this.playerHTML.displayError(err.message, await this.translationsPromise)
184 } 198 }
185 } 199 }
186 200
@@ -205,8 +219,8 @@ export class PeerTubeEmbed {
205 ? await this.videoFetcher.loadLive(videoInfo) 219 ? await this.videoFetcher.loadLive(videoInfo)
206 : undefined 220 : undefined
207 221
208 const videoFileToken = videoRequiresAuth(videoInfo) 222 const videoFileToken = videoRequiresFileToken(videoInfo)
209 ? await this.videoFetcher.loadVideoToken(videoInfo) 223 ? await this.videoFetcher.loadVideoToken(videoInfo, this.videoPassword)
210 : undefined 224 : undefined
211 225
212 return { live, video: videoInfo, videoFileToken } 226 return { live, video: videoInfo, videoFileToken }
@@ -232,6 +246,8 @@ export class PeerTubeEmbed {
232 246
233 authorizationHeader: () => this.http.getHeaderTokenValue(), 247 authorizationHeader: () => this.http.getHeaderTokenValue(),
234 videoFileToken: () => videoFileToken, 248 videoFileToken: () => videoFileToken,
249 videoPassword: () => this.videoPassword,
250 requiresPassword: this.requiresPassword,
235 251
236 onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }), 252 onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, autoplayFromPreviousVideo: true, forceAutoplay: false }),
237 253
@@ -263,6 +279,7 @@ export class PeerTubeEmbed {
263 this.initializeApi() 279 this.initializeApi()
264 280
265 this.playerHTML.removePlaceholder() 281 this.playerHTML.removePlaceholder()
282 if (this.videoPassword) this.playerHTML.removeVideoPasswordBlock()
266 283
267 if (this.isPlaylistEmbed()) { 284 if (this.isPlaylistEmbed()) {
268 await this.buildPlayerPlaylistUpnext() 285 await this.buildPlayerPlaylistUpnext()
@@ -401,6 +418,21 @@ export class PeerTubeEmbed {
401 (this.player.el() as HTMLElement).style.pointerEvents = 'none' 418 (this.player.el() as HTMLElement).style.pointerEvents = 'none'
402 } 419 }
403 420
421 private async handlePasswordError (err: PeerTubeServerError) {
422 let incorrectPassword: boolean = null
423 if (err.serverCode === ServerErrorCode.VIDEO_REQUIRES_PASSWORD) incorrectPassword = false
424 else if (err.serverCode === ServerErrorCode.INCORRECT_VIDEO_PASSWORD) incorrectPassword = true
425
426 if (incorrectPassword === null) return false
427
428 this.requiresPassword = true
429 this.videoPassword = await this.playerHTML.askVideoPassword({
430 incorrectPassword,
431 translations: await this.translationsPromise
432 })
433 return true
434 }
435
404} 436}
405 437
406PeerTubeEmbed.main() 438PeerTubeEmbed.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/player-html.ts b/client/src/standalone/videos/shared/player-html.ts
index d93678c10..a0846d9d7 100644
--- a/client/src/standalone/videos/shared/player-html.ts
+++ b/client/src/standalone/videos/shared/player-html.ts
@@ -55,6 +55,58 @@ export class PlayerHTML {
55 this.wrapperElement.style.display = 'none' 55 this.wrapperElement.style.display = 'none'
56 } 56 }
57 57
58 async askVideoPassword (options: { incorrectPassword: boolean, translations: Translations }): Promise<string> {
59 const { incorrectPassword, translations } = options
60 return new Promise((resolve) => {
61
62 this.removePlaceholder()
63 this.wrapperElement.style.display = 'none'
64
65 const translatedTitle = peertubeTranslate('This video is password protected', translations)
66 const translatedMessage = peertubeTranslate('You need a password to watch this video.', translations)
67
68 document.title = translatedTitle
69
70 const videoPasswordBlock = document.getElementById('video-password-block')
71 videoPasswordBlock.style.display = 'flex'
72
73 const videoPasswordTitle = document.getElementById('video-password-title')
74 videoPasswordTitle.innerHTML = translatedTitle
75
76 const videoPasswordMessage = document.getElementById('video-password-content')
77 videoPasswordMessage.innerHTML = translatedMessage
78
79 if (incorrectPassword) {
80 const videoPasswordError = document.getElementById('video-password-error')
81 videoPasswordError.innerHTML = peertubeTranslate('Incorrect password, please enter a correct password', translations)
82 videoPasswordError.style.transform = 'scale(1.2)'
83
84 setTimeout(() => {
85 videoPasswordError.style.transform = 'scale(1)'
86 }, 500)
87 }
88
89 const videoPasswordSubmitButton = document.getElementById('video-password-submit')
90 videoPasswordSubmitButton.innerHTML = peertubeTranslate('Watch Video', translations)
91
92 const videoPasswordInput = document.getElementById('video-password-input') as HTMLInputElement
93 videoPasswordInput.placeholder = peertubeTranslate('Password', translations)
94
95 const videoPasswordForm = document.getElementById('video-password-form')
96 videoPasswordForm.addEventListener('submit', (event) => {
97 event.preventDefault()
98 const videoPassword = videoPasswordInput.value
99 resolve(videoPassword)
100 })
101 })
102 }
103
104 removeVideoPasswordBlock () {
105 const videoPasswordBlock = document.getElementById('video-password-block')
106 videoPasswordBlock.style.display = 'none'
107 this.wrapperElement.style.display = 'block'
108 }
109
58 buildPlaceholder (video: VideoDetails) { 110 buildPlaceholder (video: VideoDetails) {
59 const placeholder = this.getPlaceholderElement() 111 const placeholder = this.getPlaceholderElement()
60 112
diff --git a/client/src/standalone/videos/shared/player-manager-options.ts b/client/src/standalone/videos/shared/player-manager-options.ts
index 43ae22a3b..587516410 100644
--- a/client/src/standalone/videos/shared/player-manager-options.ts
+++ b/client/src/standalone/videos/shared/player-manager-options.ts
@@ -18,7 +18,7 @@ import {
18 logger, 18 logger,
19 peertubeLocalStorage, 19 peertubeLocalStorage,
20 UserLocalStorageKeys, 20 UserLocalStorageKeys,
21 videoRequiresAuth 21 videoRequiresUserAuth
22} from '../../../root-helpers' 22} from '../../../root-helpers'
23import { PeerTubePlugin } from './peertube-plugin' 23import { PeerTubePlugin } from './peertube-plugin'
24import { PlayerHTML } from './player-html' 24import { PlayerHTML } from './player-html'
@@ -162,6 +162,9 @@ export class PlayerManagerOptions {
162 authorizationHeader: () => string 162 authorizationHeader: () => string
163 videoFileToken: () => string 163 videoFileToken: () => string
164 164
165 videoPassword: () => string
166 requiresPassword: boolean
167
165 serverConfig: HTMLServerConfig 168 serverConfig: HTMLServerConfig
166 169
167 autoplayFromPreviousVideo: boolean 170 autoplayFromPreviousVideo: boolean
@@ -178,6 +181,8 @@ export class PlayerManagerOptions {
178 captionsResponse, 181 captionsResponse,
179 autoplayFromPreviousVideo, 182 autoplayFromPreviousVideo,
180 videoFileToken, 183 videoFileToken,
184 videoPassword,
185 requiresPassword,
181 translations, 186 translations,
182 forceAutoplay, 187 forceAutoplay,
183 playlistTracker, 188 playlistTracker,
@@ -242,10 +247,13 @@ export class PlayerManagerOptions {
242 embedUrl: window.location.origin + video.embedPath, 247 embedUrl: window.location.origin + video.embedPath,
243 embedTitle: video.name, 248 embedTitle: video.name,
244 249
245 requiresAuth: videoRequiresAuth(video), 250 requiresUserAuth: videoRequiresUserAuth(video),
246 authorizationHeader, 251 authorizationHeader,
247 videoFileToken, 252 videoFileToken,
248 253
254 requiresPassword,
255 videoPassword,
256
249 errorNotifier: () => { 257 errorNotifier: () => {
250 // Empty, we don't have a notifier in the embed 258 // Empty, we don't have a notifier in the embed
251 }, 259 },
diff --git a/client/src/standalone/videos/shared/video-fetcher.ts b/client/src/standalone/videos/shared/video-fetcher.ts
index cf6d12831..76ba0a3ed 100644
--- a/client/src/standalone/videos/shared/video-fetcher.ts
+++ b/client/src/standalone/videos/shared/video-fetcher.ts
@@ -1,3 +1,4 @@
1import { PeerTubeServerError } from '../../../types'
1import { HttpStatusCode, LiveVideo, VideoDetails, VideoToken } from '../../../../../shared/models' 2import { HttpStatusCode, LiveVideo, VideoDetails, VideoToken } from '../../../../../shared/models'
2import { logger } from '../../../root-helpers' 3import { logger } from '../../../root-helpers'
3import { AuthHTTP } from './auth-http' 4import { AuthHTTP } from './auth-http'
@@ -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,11 +28,14 @@ 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 })
35 39
36 return { captionsPromise, videoResponse } 40 return { captionsPromise, videoResponse }
37 } 41 }
@@ -41,8 +45,8 @@ export class VideoFetcher {
41 .then(res => res.json() as Promise<LiveVideo>) 45 .then(res => res.json() as Promise<LiveVideo>)
42 } 46 }
43 47
44 loadVideoToken (video: VideoDetails) { 48 loadVideoToken (video: VideoDetails, videoPassword?: string) {
45 return this.http.fetch(this.getVideoTokenUrl(video.uuid), { optionalAuth: true, method: 'POST' }) 49 return this.http.fetch(this.getVideoTokenUrl(video.uuid), { optionalAuth: true, method: 'POST' }, videoPassword)
46 .then(res => res.json() as Promise<VideoToken>) 50 .then(res => res.json() as Promise<VideoToken>)
47 .then(token => token.files.token) 51 .then(token => token.files.token)
48 } 52 }
@@ -51,12 +55,12 @@ export class VideoFetcher {
51 return this.getVideoUrl(videoUUID) + '/views' 55 return this.getVideoUrl(videoUUID) + '/views'
52 } 56 }
53 57
54 private loadVideoInfo (videoId: string): Promise<Response> { 58 private loadVideoInfo ({ videoId, videoPassword }: { videoId: string, videoPassword?: string }): Promise<Response> {
55 return this.http.fetch(this.getVideoUrl(videoId), { optionalAuth: true }) 59 return this.http.fetch(this.getVideoUrl(videoId), { optionalAuth: true }, videoPassword)
56 } 60 }
57 61
58 private loadVideoCaptions (videoId: string): Promise<Response> { 62 private loadVideoCaptions ({ videoId, videoPassword }: { videoId: string, videoPassword?: string }): Promise<Response> {
59 return this.http.fetch(this.getVideoUrl(videoId) + '/captions', { optionalAuth: true }) 63 return this.http.fetch(this.getVideoUrl(videoId) + '/captions', { optionalAuth: true }, videoPassword)
60 } 64 }
61 65
62 private getVideoUrl (id: string) { 66 private getVideoUrl (id: string) {