aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src
diff options
context:
space:
mode:
Diffstat (limited to 'client/src')
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.ts53
-rw-r--r--client/src/app/core/auth/auth-user.model.ts18
-rw-r--r--client/src/app/core/auth/auth.service.ts4
-rw-r--r--client/src/app/core/users/user-local-storage.service.ts14
-rw-r--r--client/src/app/helpers/utils/url.ts5
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts11
-rw-r--r--client/src/app/shared/shared-main/video/index.ts1
-rw-r--r--client/src/app/shared/shared-main/video/video-file-token.service.ts33
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.html5
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.ts19
-rw-r--r--client/src/assets/player/shared/common/utils.ts8
-rw-r--r--client/src/assets/player/shared/manager-options/hls-options-builder.ts18
-rw-r--r--client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts15
-rw-r--r--client/src/assets/player/shared/p2p-media-loader/segment-validator.ts30
-rw-r--r--client/src/assets/player/shared/peertube/peertube-plugin.ts4
-rw-r--r--client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts22
-rw-r--r--client/src/assets/player/types/manager-options.ts4
-rw-r--r--client/src/assets/player/types/peertube-videojs-typings.ts7
-rw-r--r--client/src/root-helpers/logger.ts4
-rw-r--r--client/src/root-helpers/users/index.ts2
-rw-r--r--client/src/root-helpers/users/oauth-user-tokens.ts (renamed from client/src/root-helpers/users/user-tokens.ts)8
-rw-r--r--client/src/root-helpers/video.ts9
-rw-r--r--client/src/standalone/videos/embed.ts22
-rw-r--r--client/src/standalone/videos/shared/auth-http.ts28
-rw-r--r--client/src/standalone/videos/shared/player-manager-options.ts12
-rw-r--r--client/src/standalone/videos/shared/video-fetcher.ts17
26 files changed, 274 insertions, 99 deletions
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts
index 9ae6f9f12..b3818c8de 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.component.ts
@@ -20,12 +20,12 @@ import {
20} from '@app/core' 20} from '@app/core'
21import { HooksService } from '@app/core/plugins/hooks.service' 21import { HooksService } from '@app/core/plugins/hooks.service'
22import { isXPercentInViewport, scrollToTop } from '@app/helpers' 22import { isXPercentInViewport, scrollToTop } from '@app/helpers'
23import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' 23import { Video, VideoCaptionService, VideoDetails, VideoFileTokenService, VideoService } from '@app/shared/shared-main'
24import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' 24import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
25import { LiveVideoService } from '@app/shared/shared-video-live' 25import { LiveVideoService } from '@app/shared/shared-video-live'
26import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' 26import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
27import { logger } from '@root-helpers/logger' 27import { logger } from '@root-helpers/logger'
28import { isP2PEnabled } from '@root-helpers/video' 28import { isP2PEnabled, videoRequiresAuth } from '@root-helpers/video'
29import { timeToInt } from '@shared/core-utils' 29import { timeToInt } from '@shared/core-utils'
30import { 30import {
31 HTMLServerConfig, 31 HTMLServerConfig,
@@ -78,6 +78,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
78 private nextVideoUUID = '' 78 private nextVideoUUID = ''
79 private nextVideoTitle = '' 79 private nextVideoTitle = ''
80 80
81 private videoFileToken: string
82
81 private currentTime: number 83 private currentTime: number
82 84
83 private paramsSub: Subscription 85 private paramsSub: Subscription
@@ -110,6 +112,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
110 private pluginService: PluginService, 112 private pluginService: PluginService,
111 private peertubeSocket: PeerTubeSocket, 113 private peertubeSocket: PeerTubeSocket,
112 private screenService: ScreenService, 114 private screenService: ScreenService,
115 private videoFileTokenService: VideoFileTokenService,
113 private location: PlatformLocation, 116 private location: PlatformLocation,
114 @Inject(LOCALE_ID) private localeId: string 117 @Inject(LOCALE_ID) private localeId: string
115 ) { } 118 ) { }
@@ -252,12 +255,19 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
252 'filter:api.video-watch.video.get.result' 255 'filter:api.video-watch.video.get.result'
253 ) 256 )
254 257
255 const videoAndLiveObs: Observable<{ video: VideoDetails, live?: LiveVideo }> = videoObs.pipe( 258 const videoAndLiveObs: Observable<{ video: VideoDetails, live?: LiveVideo, videoFileToken?: string }> = videoObs.pipe(
256 switchMap(video => { 259 switchMap(video => {
257 if (!video.isLive) return of({ video }) 260 if (!video.isLive) return of({ video, live: undefined })
258 261
259 return this.liveVideoService.getVideoLive(video.uuid) 262 return this.liveVideoService.getVideoLive(video.uuid)
260 .pipe(map(live => ({ live, video }))) 263 .pipe(map(live => ({ live, video })))
264 }),
265
266 switchMap(({ video, live }) => {
267 if (!videoRequiresAuth(video)) return of({ video, live, videoFileToken: undefined })
268
269 return this.videoFileTokenService.getVideoFileToken(video.uuid)
270 .pipe(map(({ token }) => ({ video, live, videoFileToken: token })))
261 }) 271 })
262 ) 272 )
263 273
@@ -266,7 +276,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
266 this.videoCaptionService.listCaptions(videoId), 276 this.videoCaptionService.listCaptions(videoId),
267 this.userService.getAnonymousOrLoggedUser() 277 this.userService.getAnonymousOrLoggedUser()
268 ]).subscribe({ 278 ]).subscribe({
269 next: ([ { video, live }, captionsResult, loggedInOrAnonymousUser ]) => { 279 next: ([ { video, live, videoFileToken }, captionsResult, loggedInOrAnonymousUser ]) => {
270 const queryParams = this.route.snapshot.queryParams 280 const queryParams = this.route.snapshot.queryParams
271 281
272 const urlOptions = { 282 const urlOptions = {
@@ -283,7 +293,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
283 peertubeLink: false 293 peertubeLink: false
284 } 294 }
285 295
286 this.onVideoFetched({ video, live, videoCaptions: captionsResult.data, loggedInOrAnonymousUser, urlOptions }) 296 this.onVideoFetched({ video, live, videoCaptions: captionsResult.data, videoFileToken, loggedInOrAnonymousUser, urlOptions })
287 .catch(err => this.handleGlobalError(err)) 297 .catch(err => this.handleGlobalError(err))
288 }, 298 },
289 299
@@ -356,16 +366,19 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
356 video: VideoDetails 366 video: VideoDetails
357 live: LiveVideo 367 live: LiveVideo
358 videoCaptions: VideoCaption[] 368 videoCaptions: VideoCaption[]
369 videoFileToken: string
370
359 urlOptions: URLOptions 371 urlOptions: URLOptions
360 loggedInOrAnonymousUser: User 372 loggedInOrAnonymousUser: User
361 }) { 373 }) {
362 const { video, live, videoCaptions, urlOptions, loggedInOrAnonymousUser } = options 374 const { video, live, videoCaptions, urlOptions, videoFileToken, loggedInOrAnonymousUser } = options
363 375
364 this.subscribeToLiveEventsIfNeeded(this.video, video) 376 this.subscribeToLiveEventsIfNeeded(this.video, video)
365 377
366 this.video = video 378 this.video = video
367 this.videoCaptions = videoCaptions 379 this.videoCaptions = videoCaptions
368 this.liveVideo = live 380 this.liveVideo = live
381 this.videoFileToken = videoFileToken
369 382
370 // Re init attributes 383 // Re init attributes
371 this.playerPlaceholderImgSrc = undefined 384 this.playerPlaceholderImgSrc = undefined
@@ -414,6 +427,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
414 video: this.video, 427 video: this.video,
415 videoCaptions: this.videoCaptions, 428 videoCaptions: this.videoCaptions,
416 liveVideo: this.liveVideo, 429 liveVideo: this.liveVideo,
430 videoFileToken: this.videoFileToken,
417 urlOptions, 431 urlOptions,
418 loggedInOrAnonymousUser, 432 loggedInOrAnonymousUser,
419 user: this.user 433 user: this.user
@@ -561,11 +575,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
561 video: VideoDetails 575 video: VideoDetails
562 liveVideo: LiveVideo 576 liveVideo: LiveVideo
563 videoCaptions: VideoCaption[] 577 videoCaptions: VideoCaption[]
578
579 videoFileToken: string
580
564 urlOptions: CustomizationOptions & { playerMode: PlayerMode } 581 urlOptions: CustomizationOptions & { playerMode: PlayerMode }
582
565 loggedInOrAnonymousUser: User 583 loggedInOrAnonymousUser: User
566 user?: AuthUser // Keep for plugins 584 user?: AuthUser // Keep for plugins
567 }) { 585 }) {
568 const { video, liveVideo, videoCaptions, urlOptions, loggedInOrAnonymousUser } = params 586 const { video, liveVideo, videoCaptions, videoFileToken, urlOptions, loggedInOrAnonymousUser } = params
569 587
570 const getStartTime = () => { 588 const getStartTime = () => {
571 const byUrl = urlOptions.startTime !== undefined 589 const byUrl = urlOptions.startTime !== undefined
@@ -623,13 +641,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
623 theaterButton: true, 641 theaterButton: true,
624 captions: videoCaptions.length !== 0, 642 captions: videoCaptions.length !== 0,
625 643
626 videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE
627 ? this.videoService.getVideoViewUrl(video.uuid)
628 : null,
629 authorizationHeader: this.authService.getRequestHeaderValue(),
630
631 metricsUrl: environment.apiUrl + '/api/v1/metrics/playback',
632
633 embedUrl: video.embedUrl, 644 embedUrl: video.embedUrl,
634 embedTitle: video.name, 645 embedTitle: video.name,
635 instanceName: this.serverConfig.instance.name, 646 instanceName: this.serverConfig.instance.name,
@@ -639,7 +650,17 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
639 650
640 language: this.localeId, 651 language: this.localeId,
641 652
642 serverUrl: environment.apiUrl, 653 metricsUrl: environment.apiUrl + '/api/v1/metrics/playback',
654
655 videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE
656 ? this.videoService.getVideoViewUrl(video.uuid)
657 : null,
658 authorizationHeader: () => this.authService.getRequestHeaderValue(),
659
660 serverUrl: environment.originServerUrl,
661
662 videoFileToken: () => videoFileToken,
663 requiresAuth: videoRequiresAuth(video),
643 664
644 videoCaptions: playerCaptions, 665 videoCaptions: playerCaptions,
645 666
diff --git a/client/src/app/core/auth/auth-user.model.ts b/client/src/app/core/auth/auth-user.model.ts
index cd9665e37..a12325421 100644
--- a/client/src/app/core/auth/auth-user.model.ts
+++ b/client/src/app/core/auth/auth-user.model.ts
@@ -1,7 +1,7 @@
1import { Observable, of } from 'rxjs' 1import { Observable, of } from 'rxjs'
2import { map } from 'rxjs/operators' 2import { map } from 'rxjs/operators'
3import { User } from '@app/core/users/user.model' 3import { User } from '@app/core/users/user.model'
4import { UserTokens } from '@root-helpers/users' 4import { OAuthUserTokens } from '@root-helpers/users'
5import { hasUserRight } from '@shared/core-utils/users' 5import { hasUserRight } from '@shared/core-utils/users'
6import { 6import {
7 MyUser as ServerMyUserModel, 7 MyUser as ServerMyUserModel,
@@ -13,33 +13,33 @@ import {
13} from '@shared/models' 13} from '@shared/models'
14 14
15export class AuthUser extends User implements ServerMyUserModel { 15export class AuthUser extends User implements ServerMyUserModel {
16 tokens: UserTokens 16 oauthTokens: OAuthUserTokens
17 specialPlaylists: MyUserSpecialPlaylist[] 17 specialPlaylists: MyUserSpecialPlaylist[]
18 18
19 canSeeVideosLink = true 19 canSeeVideosLink = true
20 20
21 constructor (userHash: Partial<ServerMyUserModel>, hashTokens: Partial<UserTokens>) { 21 constructor (userHash: Partial<ServerMyUserModel>, hashTokens: Partial<OAuthUserTokens>) {
22 super(userHash) 22 super(userHash)
23 23
24 this.tokens = new UserTokens(hashTokens) 24 this.oauthTokens = new OAuthUserTokens(hashTokens)
25 this.specialPlaylists = userHash.specialPlaylists 25 this.specialPlaylists = userHash.specialPlaylists
26 } 26 }
27 27
28 getAccessToken () { 28 getAccessToken () {
29 return this.tokens.accessToken 29 return this.oauthTokens.accessToken
30 } 30 }
31 31
32 getRefreshToken () { 32 getRefreshToken () {
33 return this.tokens.refreshToken 33 return this.oauthTokens.refreshToken
34 } 34 }
35 35
36 getTokenType () { 36 getTokenType () {
37 return this.tokens.tokenType 37 return this.oauthTokens.tokenType
38 } 38 }
39 39
40 refreshTokens (accessToken: string, refreshToken: string) { 40 refreshTokens (accessToken: string, refreshToken: string) {
41 this.tokens.accessToken = accessToken 41 this.oauthTokens.accessToken = accessToken
42 this.tokens.refreshToken = refreshToken 42 this.oauthTokens.refreshToken = refreshToken
43 } 43 }
44 44
45 hasRight (right: UserRight) { 45 hasRight (right: UserRight) {
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts
index 7f4fae4aa..4de28e51e 100644
--- a/client/src/app/core/auth/auth.service.ts
+++ b/client/src/app/core/auth/auth.service.ts
@@ -5,7 +5,7 @@ import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular
5import { Injectable } from '@angular/core' 5import { Injectable } from '@angular/core'
6import { Router } from '@angular/router' 6import { Router } from '@angular/router'
7import { Notifier } from '@app/core/notification/notifier.service' 7import { Notifier } from '@app/core/notification/notifier.service'
8import { logger, objectToUrlEncoded, peertubeLocalStorage, UserTokens } from '@root-helpers/index' 8import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage } from '@root-helpers/index'
9import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' 9import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models'
10import { environment } from '../../../environments/environment' 10import { environment } from '../../../environments/environment'
11import { RestExtractor } from '../rest/rest-extractor.service' 11import { RestExtractor } from '../rest/rest-extractor.service'
@@ -74,7 +74,7 @@ export class AuthService {
74 ] 74 ]
75 } 75 }
76 76
77 buildAuthUser (userInfo: Partial<User>, tokens: UserTokens) { 77 buildAuthUser (userInfo: Partial<User>, tokens: OAuthUserTokens) {
78 this.user = new AuthUser(userInfo, tokens) 78 this.user = new AuthUser(userInfo, tokens)
79 } 79 }
80 80
diff --git a/client/src/app/core/users/user-local-storage.service.ts b/client/src/app/core/users/user-local-storage.service.ts
index fff649eef..f1588bdd2 100644
--- a/client/src/app/core/users/user-local-storage.service.ts
+++ b/client/src/app/core/users/user-local-storage.service.ts
@@ -4,7 +4,7 @@ import { Injectable } from '@angular/core'
4import { AuthService, AuthStatus } from '@app/core/auth' 4import { AuthService, AuthStatus } from '@app/core/auth'
5import { getBoolOrDefault } from '@root-helpers/local-storage-utils' 5import { getBoolOrDefault } from '@root-helpers/local-storage-utils'
6import { logger } from '@root-helpers/logger' 6import { logger } from '@root-helpers/logger'
7import { UserLocalStorageKeys, UserTokens } from '@root-helpers/users' 7import { UserLocalStorageKeys, OAuthUserTokens } from '@root-helpers/users'
8import { UserRole, UserUpdateMe } from '@shared/models' 8import { UserRole, UserUpdateMe } from '@shared/models'
9import { NSFWPolicyType } from '@shared/models/videos' 9import { NSFWPolicyType } from '@shared/models/videos'
10import { ServerService } from '../server' 10import { ServerService } from '../server'
@@ -24,7 +24,7 @@ export class UserLocalStorageService {
24 24
25 this.setLoggedInUser(user) 25 this.setLoggedInUser(user)
26 this.setUserInfo(user) 26 this.setUserInfo(user)
27 this.setTokens(user.tokens) 27 this.setTokens(user.oauthTokens)
28 } 28 }
29 }) 29 })
30 30
@@ -43,7 +43,7 @@ export class UserLocalStorageService {
43 next: () => { 43 next: () => {
44 const user = this.authService.getUser() 44 const user = this.authService.getUser()
45 45
46 this.setTokens(user.tokens) 46 this.setTokens(user.oauthTokens)
47 } 47 }
48 }) 48 })
49 } 49 }
@@ -174,14 +174,14 @@ export class UserLocalStorageService {
174 // --------------------------------------------------------------------------- 174 // ---------------------------------------------------------------------------
175 175
176 getTokens () { 176 getTokens () {
177 return UserTokens.getUserTokens(this.localStorageService) 177 return OAuthUserTokens.getUserTokens(this.localStorageService)
178 } 178 }
179 179
180 setTokens (tokens: UserTokens) { 180 setTokens (tokens: OAuthUserTokens) {
181 UserTokens.saveToLocalStorage(this.localStorageService, tokens) 181 OAuthUserTokens.saveToLocalStorage(this.localStorageService, tokens)
182 } 182 }
183 183
184 flushTokens () { 184 flushTokens () {
185 UserTokens.flushLocalStorage(this.localStorageService) 185 OAuthUserTokens.flushLocalStorage(this.localStorageService)
186 } 186 }
187} 187}
diff --git a/client/src/app/helpers/utils/url.ts b/client/src/app/helpers/utils/url.ts
index 08c27e3c1..9e7dc3e6f 100644
--- a/client/src/app/helpers/utils/url.ts
+++ b/client/src/app/helpers/utils/url.ts
@@ -54,8 +54,9 @@ function objectToFormData (obj: any, form?: FormData, namespace?: string) {
54} 54}
55 55
56export { 56export {
57 objectToFormData,
58 getAbsoluteAPIUrl, 57 getAbsoluteAPIUrl,
59 getAPIHost, 58 getAPIHost,
60 getAbsoluteEmbedUrl 59 getAbsoluteEmbedUrl,
60
61 objectToFormData
61} 62}
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts
index 04b223cc5..c1523bc50 100644
--- a/client/src/app/shared/shared-main/shared-main.module.ts
+++ b/client/src/app/shared/shared-main/shared-main.module.ts
@@ -44,7 +44,15 @@ import {
44import { PluginPlaceholderComponent, PluginSelectorDirective } from './plugins' 44import { PluginPlaceholderComponent, PluginSelectorDirective } from './plugins'
45import { ActorRedirectGuard } from './router' 45import { ActorRedirectGuard } from './router'
46import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' 46import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users'
47import { EmbedComponent, RedundancyService, VideoImportService, VideoOwnershipService, VideoResolver, VideoService } from './video' 47import {
48 EmbedComponent,
49 RedundancyService,
50 VideoFileTokenService,
51 VideoImportService,
52 VideoOwnershipService,
53 VideoResolver,
54 VideoService
55} from './video'
48import { VideoCaptionService } from './video-caption' 56import { VideoCaptionService } from './video-caption'
49import { VideoChannelService } from './video-channel' 57import { VideoChannelService } from './video-channel'
50 58
@@ -185,6 +193,7 @@ import { VideoChannelService } from './video-channel'
185 VideoImportService, 193 VideoImportService,
186 VideoOwnershipService, 194 VideoOwnershipService,
187 VideoService, 195 VideoService,
196 VideoFileTokenService,
188 VideoResolver, 197 VideoResolver,
189 198
190 VideoCaptionService, 199 VideoCaptionService,
diff --git a/client/src/app/shared/shared-main/video/index.ts b/client/src/app/shared/shared-main/video/index.ts
index 361601456..a2e47883e 100644
--- a/client/src/app/shared/shared-main/video/index.ts
+++ b/client/src/app/shared/shared-main/video/index.ts
@@ -2,6 +2,7 @@ export * from './embed.component'
2export * from './redundancy.service' 2export * from './redundancy.service'
3export * from './video-details.model' 3export * from './video-details.model'
4export * from './video-edit.model' 4export * from './video-edit.model'
5export * from './video-file-token.service'
5export * from './video-import.service' 6export * from './video-import.service'
6export * from './video-ownership.service' 7export * from './video-ownership.service'
7export * from './video.model' 8export * from './video.model'
diff --git a/client/src/app/shared/shared-main/video/video-file-token.service.ts b/client/src/app/shared/shared-main/video/video-file-token.service.ts
new file mode 100644
index 000000000..791607249
--- /dev/null
+++ b/client/src/app/shared/shared-main/video/video-file-token.service.ts
@@ -0,0 +1,33 @@
1import { catchError, map, of, tap } from 'rxjs'
2import { HttpClient } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { RestExtractor } from '@app/core'
5import { VideoToken } from '@shared/models'
6import { VideoService } from './video.service'
7
8@Injectable()
9export class VideoFileTokenService {
10
11 private readonly store = new Map<string, { token: string, expires: Date }>()
12
13 constructor (
14 private authHttp: HttpClient,
15 private restExtractor: RestExtractor
16 ) {}
17
18 getVideoFileToken (videoUUID: string) {
19 const existing = this.store.get(videoUUID)
20 if (existing) return of(existing)
21
22 return this.createVideoFileToken(videoUUID)
23 .pipe(tap(result => this.store.set(videoUUID, { token: result.token, expires: new Date(result.expires) })))
24 }
25
26 private createVideoFileToken (videoUUID: string) {
27 return this.authHttp.post<VideoToken>(`${VideoService.BASE_VIDEO_URL}/${videoUUID}/token`, {})
28 .pipe(
29 map(({ files }) => files),
30 catchError(err => this.restExtractor.handleError(err))
31 )
32 }
33}
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.html b/client/src/app/shared/shared-video-miniature/video-download.component.html
index 1c7458b4b..1f622933d 100644
--- a/client/src/app/shared/shared-video-miniature/video-download.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-download.component.html
@@ -48,10 +48,7 @@
48 48
49 <ng-template ngbNavContent> 49 <ng-template ngbNavContent>
50 <div class="nav-content"> 50 <div class="nav-content">
51 <my-input-text 51 <my-input-text [show]="true" [readonly]="true" [withCopy]="true" [withToggle]="false" [value]="getLink()"></my-input-text>
52 *ngIf="!isConfidentialVideo()"
53 [show]="true" [readonly]="true" [withCopy]="true" [withToggle]="false" [value]="getLink()"
54 ></my-input-text>
55 </div> 52 </div>
56 </ng-template> 53 </ng-template>
57 </ng-container> 54 </ng-container>
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.ts b/client/src/app/shared/shared-video-miniature/video-download.component.ts
index 47482caaa..667cb107f 100644
--- a/client/src/app/shared/shared-video-miniature/video-download.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-download.component.ts
@@ -2,11 +2,12 @@ import { mapValues, pick } from 'lodash-es'
2import { firstValueFrom } from 'rxjs' 2import { firstValueFrom } from 'rxjs'
3import { tap } from 'rxjs/operators' 3import { tap } from 'rxjs/operators'
4import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' 4import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core'
5import { AuthService, HooksService, Notifier } from '@app/core' 5import { HooksService } from '@app/core'
6import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
7import { logger } from '@root-helpers/logger' 7import { logger } from '@root-helpers/logger'
8import { videoRequiresAuth } from '@root-helpers/video'
8import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' 9import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models'
9import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main' 10import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoFileTokenService, VideoService } from '../shared-main'
10 11
11type DownloadType = 'video' | 'subtitles' 12type DownloadType = 'video' | 'subtitles'
12type FileMetadata = { [key: string]: { label: string, value: string }} 13type FileMetadata = { [key: string]: { label: string, value: string }}
@@ -32,6 +33,8 @@ export class VideoDownloadComponent {
32 33
33 type: DownloadType = 'video' 34 type: DownloadType = 'video'
34 35
36 videoFileToken: string
37
35 private activeModal: NgbModalRef 38 private activeModal: NgbModalRef
36 39
37 private bytesPipe: BytesPipe 40 private bytesPipe: BytesPipe
@@ -42,10 +45,9 @@ export class VideoDownloadComponent {
42 45
43 constructor ( 46 constructor (
44 @Inject(LOCALE_ID) private localeId: string, 47 @Inject(LOCALE_ID) private localeId: string,
45 private notifier: Notifier,
46 private modalService: NgbModal, 48 private modalService: NgbModal,
47 private videoService: VideoService, 49 private videoService: VideoService,
48 private auth: AuthService, 50 private videoFileTokenService: VideoFileTokenService,
49 private hooks: HooksService 51 private hooks: HooksService
50 ) { 52 ) {
51 this.bytesPipe = new BytesPipe() 53 this.bytesPipe = new BytesPipe()
@@ -71,6 +73,8 @@ export class VideoDownloadComponent {
71 } 73 }
72 74
73 show (video: VideoDetails, videoCaptions?: VideoCaption[]) { 75 show (video: VideoDetails, videoCaptions?: VideoCaption[]) {
76 this.videoFileToken = undefined
77
74 this.video = video 78 this.video = video
75 this.videoCaptions = videoCaptions 79 this.videoCaptions = videoCaptions
76 80
@@ -84,6 +88,11 @@ export class VideoDownloadComponent {
84 this.subtitleLanguageId = this.videoCaptions[0].language.id 88 this.subtitleLanguageId = this.videoCaptions[0].language.id
85 } 89 }
86 90
91 if (videoRequiresAuth(this.video)) {
92 this.videoFileTokenService.getVideoFileToken(this.video.uuid)
93 .subscribe(({ token }) => this.videoFileToken = token)
94 }
95
87 this.activeModal.shown.subscribe(() => { 96 this.activeModal.shown.subscribe(() => {
88 this.hooks.runAction('action:modal.video-download.shown', 'common') 97 this.hooks.runAction('action:modal.video-download.shown', 'common')
89 }) 98 })
@@ -155,7 +164,7 @@ export class VideoDownloadComponent {
155 if (!file) return '' 164 if (!file) return ''
156 165
157 const suffix = this.isConfidentialVideo() 166 const suffix = this.isConfidentialVideo()
158 ? '?access_token=' + this.auth.getAccessToken() 167 ? '?videoFileToken=' + this.videoFileToken
159 : '' 168 : ''
160 169
161 switch (this.downloadType) { 170 switch (this.downloadType) {
diff --git a/client/src/assets/player/shared/common/utils.ts b/client/src/assets/player/shared/common/utils.ts
index a010d9184..609240626 100644
--- a/client/src/assets/player/shared/common/utils.ts
+++ b/client/src/assets/player/shared/common/utils.ts
@@ -52,6 +52,10 @@ function getRtcConfig () {
52 } 52 }
53} 53}
54 54
55function isSameOrigin (current: string, target: string) {
56 return new URL(current).origin === new URL(target).origin
57}
58
55// --------------------------------------------------------------------------- 59// ---------------------------------------------------------------------------
56 60
57export { 61export {
@@ -60,5 +64,7 @@ export {
60 64
61 videoFileMaxByResolution, 65 videoFileMaxByResolution,
62 videoFileMinByResolution, 66 videoFileMinByResolution,
63 bytes 67 bytes,
68
69 isSameOrigin
64} 70}
diff --git a/client/src/assets/player/shared/manager-options/hls-options-builder.ts b/client/src/assets/player/shared/manager-options/hls-options-builder.ts
index 361c76f4b..933c0d595 100644
--- a/client/src/assets/player/shared/manager-options/hls-options-builder.ts
+++ b/client/src/assets/player/shared/manager-options/hls-options-builder.ts
@@ -5,7 +5,7 @@ import { LiveVideoLatencyMode } from '@shared/models'
5import { getAverageBandwidthInStore } from '../../peertube-player-local-storage' 5import { getAverageBandwidthInStore } from '../../peertube-player-local-storage'
6import { P2PMediaLoader, P2PMediaLoaderPluginOptions } from '../../types' 6import { P2PMediaLoader, P2PMediaLoaderPluginOptions } from '../../types'
7import { PeertubePlayerManagerOptions } from '../../types/manager-options' 7import { PeertubePlayerManagerOptions } from '../../types/manager-options'
8import { getRtcConfig } from '../common' 8import { getRtcConfig, isSameOrigin } from '../common'
9import { RedundancyUrlManager } from '../p2p-media-loader/redundancy-url-manager' 9import { RedundancyUrlManager } from '../p2p-media-loader/redundancy-url-manager'
10import { segmentUrlBuilderFactory } from '../p2p-media-loader/segment-url-builder' 10import { segmentUrlBuilderFactory } from '../p2p-media-loader/segment-url-builder'
11import { segmentValidatorFactory } from '../p2p-media-loader/segment-validator' 11import { segmentValidatorFactory } from '../p2p-media-loader/segment-validator'
@@ -84,7 +84,21 @@ export class HLSOptionsBuilder {
84 simultaneousHttpDownloads: 1, 84 simultaneousHttpDownloads: 1,
85 httpFailedSegmentTimeout: 1000, 85 httpFailedSegmentTimeout: 1000,
86 86
87 segmentValidator: segmentValidatorFactory(this.options.p2pMediaLoader.segmentsSha256Url, this.options.common.isLive), 87 xhrSetup: (xhr, url) => {
88 if (!this.options.common.requiresAuth) return
89 if (!isSameOrigin(this.options.common.serverUrl, url)) return
90
91 xhr.setRequestHeader('Authorization', this.options.common.authorizationHeader())
92 },
93
94 segmentValidator: segmentValidatorFactory({
95 segmentsSha256Url: this.options.p2pMediaLoader.segmentsSha256Url,
96 isLive: this.options.common.isLive,
97 authorizationHeader: this.options.common.authorizationHeader,
98 requiresAuth: this.options.common.requiresAuth,
99 serverUrl: this.options.common.serverUrl
100 }),
101
88 segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager), 102 segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager),
89 103
90 useP2P: this.options.common.p2pEnabled, 104 useP2P: this.options.common.p2pEnabled,
diff --git a/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts b/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts
index 257cf1e05..b5bdcd4e6 100644
--- a/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts
+++ b/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts
@@ -1,4 +1,5 @@
1import { PeertubePlayerManagerOptions } from '../../types' 1import { addQueryParams } from '../../../../../../shared/core-utils'
2import { PeertubePlayerManagerOptions, WebtorrentPluginOptions } from '../../types'
2 3
3export class WebTorrentOptionsBuilder { 4export class WebTorrentOptionsBuilder {
4 5
@@ -16,13 +17,23 @@ export class WebTorrentOptionsBuilder {
16 17
17 const autoplay = this.autoPlayValue === 'play' 18 const autoplay = this.autoPlayValue === 'play'
18 19
19 const webtorrent = { 20 const webtorrent: WebtorrentPluginOptions = {
20 autoplay, 21 autoplay,
21 22
22 playerRefusedP2P: commonOptions.p2pEnabled === false, 23 playerRefusedP2P: commonOptions.p2pEnabled === false,
23 videoDuration: commonOptions.videoDuration, 24 videoDuration: commonOptions.videoDuration,
24 playerElement: commonOptions.playerElement, 25 playerElement: commonOptions.playerElement,
25 26
27 videoFileToken: commonOptions.videoFileToken,
28
29 requiresAuth: commonOptions.requiresAuth,
30
31 buildWebSeedUrls: file => {
32 if (!commonOptions.requiresAuth) return []
33
34 return [ addQueryParams(file.fileUrl, { videoFileToken: commonOptions.videoFileToken() }) ]
35 },
36
26 videoFiles: webtorrentOptions.videoFiles.length !== 0 37 videoFiles: webtorrentOptions.videoFiles.length !== 0
27 ? webtorrentOptions.videoFiles 38 ? webtorrentOptions.videoFiles
28 // The WebTorrent plugin won't be able to play these files, but it will fallback to HTTP mode 39 // The WebTorrent plugin won't be able to play these files, but it will fallback to HTTP mode
diff --git a/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts b/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts
index 18cb6750f..a7ee91950 100644
--- a/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts
+++ b/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts
@@ -2,13 +2,22 @@ import { basename } from 'path'
2import { Segment } from '@peertube/p2p-media-loader-core' 2import { Segment } from '@peertube/p2p-media-loader-core'
3import { logger } from '@root-helpers/logger' 3import { logger } from '@root-helpers/logger'
4import { wait } from '@root-helpers/utils' 4import { wait } from '@root-helpers/utils'
5import { isSameOrigin } from '../common'
5 6
6type SegmentsJSON = { [filename: string]: string | { [byterange: string]: string } } 7type SegmentsJSON = { [filename: string]: string | { [byterange: string]: string } }
7 8
8const maxRetries = 3 9const maxRetries = 3
9 10
10function segmentValidatorFactory (segmentsSha256Url: string, isLive: boolean) { 11function segmentValidatorFactory (options: {
11 let segmentsJSON = fetchSha256Segments(segmentsSha256Url) 12 serverUrl: string
13 segmentsSha256Url: string
14 isLive: boolean
15 authorizationHeader: () => string
16 requiresAuth: boolean
17}) {
18 const { serverUrl, segmentsSha256Url, isLive, authorizationHeader, requiresAuth } = options
19
20 let segmentsJSON = fetchSha256Segments({ serverUrl, segmentsSha256Url, authorizationHeader, requiresAuth })
12 const regex = /bytes=(\d+)-(\d+)/ 21 const regex = /bytes=(\d+)-(\d+)/
13 22
14 return async function segmentValidator (segment: Segment, _method: string, _peerId: string, retry = 1) { 23 return async function segmentValidator (segment: Segment, _method: string, _peerId: string, retry = 1) {
@@ -28,7 +37,7 @@ function segmentValidatorFactory (segmentsSha256Url: string, isLive: boolean) {
28 37
29 await wait(1000) 38 await wait(1000)
30 39
31 segmentsJSON = fetchSha256Segments(segmentsSha256Url) 40 segmentsJSON = fetchSha256Segments({ serverUrl, segmentsSha256Url, authorizationHeader, requiresAuth })
32 await segmentValidator(segment, _method, _peerId, retry + 1) 41 await segmentValidator(segment, _method, _peerId, retry + 1)
33 42
34 return 43 return
@@ -68,8 +77,19 @@ export {
68 77
69// --------------------------------------------------------------------------- 78// ---------------------------------------------------------------------------
70 79
71function fetchSha256Segments (url: string) { 80function fetchSha256Segments (options: {
72 return fetch(url) 81 serverUrl: string
82 segmentsSha256Url: string
83 authorizationHeader: () => string
84 requiresAuth: boolean
85}) {
86 const { serverUrl, segmentsSha256Url, requiresAuth, authorizationHeader } = options
87
88 const headers = requiresAuth && isSameOrigin(serverUrl, segmentsSha256Url)
89 ? { Authorization: authorizationHeader() }
90 : {}
91
92 return fetch(segmentsSha256Url, { headers })
73 .then(res => res.json() as Promise<SegmentsJSON>) 93 .then(res => res.json() as Promise<SegmentsJSON>)
74 .catch(err => { 94 .catch(err => {
75 logger.error('Cannot get sha256 segments', err) 95 logger.error('Cannot get sha256 segments', err)
diff --git a/client/src/assets/player/shared/peertube/peertube-plugin.ts b/client/src/assets/player/shared/peertube/peertube-plugin.ts
index a5d712d70..4bd038bb1 100644
--- a/client/src/assets/player/shared/peertube/peertube-plugin.ts
+++ b/client/src/assets/player/shared/peertube/peertube-plugin.ts
@@ -22,7 +22,7 @@ const Plugin = videojs.getPlugin('plugin')
22 22
23class PeerTubePlugin extends Plugin { 23class PeerTubePlugin extends Plugin {
24 private readonly videoViewUrl: string 24 private readonly videoViewUrl: string
25 private readonly authorizationHeader: string 25 private readonly authorizationHeader: () => string
26 26
27 private readonly videoUUID: string 27 private readonly videoUUID: string
28 private readonly startTime: number 28 private readonly startTime: number
@@ -228,7 +228,7 @@ class PeerTubePlugin extends Plugin {
228 'Content-type': 'application/json; charset=UTF-8' 228 'Content-type': 'application/json; charset=UTF-8'
229 }) 229 })
230 230
231 if (this.authorizationHeader) headers.set('Authorization', this.authorizationHeader) 231 if (this.authorizationHeader) headers.set('Authorization', this.authorizationHeader())
232 232
233 return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers }) 233 return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers })
234 } 234 }
diff --git a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts
index fa3f48a9a..658b7c867 100644
--- a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts
+++ b/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts
@@ -2,7 +2,7 @@ import videojs from 'video.js'
2import * as WebTorrent from 'webtorrent' 2import * as WebTorrent from 'webtorrent'
3import { logger } from '@root-helpers/logger' 3import { logger } from '@root-helpers/logger'
4import { isIOS } from '@root-helpers/web-browser' 4import { isIOS } from '@root-helpers/web-browser'
5import { timeToInt } from '@shared/core-utils' 5import { addQueryParams, timeToInt } from '@shared/core-utils'
6import { VideoFile } from '@shared/models' 6import { VideoFile } from '@shared/models'
7import { getAverageBandwidthInStore, getStoredMute, getStoredVolume, saveAverageBandwidth } from '../../peertube-player-local-storage' 7import { getAverageBandwidthInStore, getStoredMute, getStoredVolume, saveAverageBandwidth } from '../../peertube-player-local-storage'
8import { PeerTubeResolution, PlayerNetworkInfo, WebtorrentPluginOptions } from '../../types' 8import { PeerTubeResolution, PlayerNetworkInfo, WebtorrentPluginOptions } from '../../types'
@@ -38,6 +38,8 @@ class WebTorrentPlugin extends Plugin {
38 BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5 // Last 5 seconds to build average bandwidth 38 BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5 // Last 5 seconds to build average bandwidth
39 } 39 }
40 40
41 private readonly buildWebSeedUrls: (file: VideoFile) => string[]
42
41 private readonly webtorrent = new WebTorrent({ 43 private readonly webtorrent = new WebTorrent({
42 tracker: { 44 tracker: {
43 rtcConfig: getRtcConfig() 45 rtcConfig: getRtcConfig()
@@ -57,6 +59,9 @@ class WebTorrentPlugin extends Plugin {
57 private isAutoResolutionObservation = false 59 private isAutoResolutionObservation = false
58 private playerRefusedP2P = false 60 private playerRefusedP2P = false
59 61
62 private requiresAuth: boolean
63 private videoFileToken: () => string
64
60 private torrentInfoInterval: any 65 private torrentInfoInterval: any
61 private autoQualityInterval: any 66 private autoQualityInterval: any
62 private addTorrentDelay: any 67 private addTorrentDelay: any
@@ -81,6 +86,11 @@ class WebTorrentPlugin extends Plugin {
81 this.savePlayerSrcFunction = this.player.src 86 this.savePlayerSrcFunction = this.player.src
82 this.playerElement = options.playerElement 87 this.playerElement = options.playerElement
83 88
89 this.requiresAuth = options.requiresAuth
90 this.videoFileToken = options.videoFileToken
91
92 this.buildWebSeedUrls = options.buildWebSeedUrls
93
84 this.player.ready(() => { 94 this.player.ready(() => {
85 const playerOptions = this.player.options_ 95 const playerOptions = this.player.options_
86 96
@@ -268,7 +278,8 @@ class WebTorrentPlugin extends Plugin {
268 return new CacheChunkStore(new PeertubeChunkStore(chunkLength, storeOpts), { 278 return new CacheChunkStore(new PeertubeChunkStore(chunkLength, storeOpts), {
269 max: 100 279 max: 100
270 }) 280 })
271 } 281 },
282 urlList: this.buildWebSeedUrls(this.currentVideoFile)
272 } 283 }
273 284
274 this.torrent = this.webtorrent.add(magnetOrTorrentUrl, torrentOptions, torrent => { 285 this.torrent = this.webtorrent.add(magnetOrTorrentUrl, torrentOptions, torrent => {
@@ -533,7 +544,12 @@ class WebTorrentPlugin extends Plugin {
533 // Enable error display now this is our last fallback 544 // Enable error display now this is our last fallback
534 this.player.one('error', () => this.player.peertube().displayFatalError()) 545 this.player.one('error', () => this.player.peertube().displayFatalError())
535 546
536 const httpUrl = this.currentVideoFile.fileUrl 547 let httpUrl = this.currentVideoFile.fileUrl
548
549 if (this.requiresAuth && this.videoFileToken) {
550 httpUrl = addQueryParams(httpUrl, { videoFileToken: this.videoFileToken() })
551 }
552
537 this.player.src = this.savePlayerSrcFunction 553 this.player.src = this.savePlayerSrcFunction
538 this.player.src(httpUrl) 554 this.player.src(httpUrl)
539 555
diff --git a/client/src/assets/player/types/manager-options.ts b/client/src/assets/player/types/manager-options.ts
index b4d9374c3..9da8fedf8 100644
--- a/client/src/assets/player/types/manager-options.ts
+++ b/client/src/assets/player/types/manager-options.ts
@@ -57,7 +57,7 @@ export interface CommonOptions extends CustomizationOptions {
57 captions: boolean 57 captions: boolean
58 58
59 videoViewUrl: string 59 videoViewUrl: string
60 authorizationHeader?: string 60 authorizationHeader?: () => string
61 61
62 metricsUrl: string 62 metricsUrl: string
63 63
@@ -77,6 +77,8 @@ export interface CommonOptions extends CustomizationOptions {
77 videoShortUUID: string 77 videoShortUUID: string
78 78
79 serverUrl: string 79 serverUrl: string
80 requiresAuth: boolean
81 videoFileToken: () => string
80 82
81 errorNotifier: (message: string) => void 83 errorNotifier: (message: string) => void
82} 84}
diff --git a/client/src/assets/player/types/peertube-videojs-typings.ts b/client/src/assets/player/types/peertube-videojs-typings.ts
index 6df94992c..037c4b74b 100644
--- a/client/src/assets/player/types/peertube-videojs-typings.ts
+++ b/client/src/assets/player/types/peertube-videojs-typings.ts
@@ -95,7 +95,7 @@ type PeerTubePluginOptions = {
95 videoDuration: number 95 videoDuration: number
96 96
97 videoViewUrl: string 97 videoViewUrl: string
98 authorizationHeader?: string 98 authorizationHeader?: () => string
99 99
100 subtitle?: string 100 subtitle?: string
101 101
@@ -151,6 +151,11 @@ type WebtorrentPluginOptions = {
151 startTime: number | string 151 startTime: number | string
152 152
153 playerRefusedP2P: boolean 153 playerRefusedP2P: boolean
154
155 requiresAuth: boolean
156 videoFileToken: () => string
157
158 buildWebSeedUrls: (file: VideoFile) => string[]
154} 159}
155 160
156type P2PMediaLoaderPluginOptions = { 161type P2PMediaLoaderPluginOptions = {
diff --git a/client/src/root-helpers/logger.ts b/client/src/root-helpers/logger.ts
index 0d486c433..d1fdf73aa 100644
--- a/client/src/root-helpers/logger.ts
+++ b/client/src/root-helpers/logger.ts
@@ -1,6 +1,6 @@
1import { ClientLogCreate } from '@shared/models/server' 1import { ClientLogCreate } from '@shared/models/server'
2import { peertubeLocalStorage } from './peertube-web-storage' 2import { peertubeLocalStorage } from './peertube-web-storage'
3import { UserTokens } from './users' 3import { OAuthUserTokens } from './users'
4 4
5export type LoggerHook = (message: LoggerMessage, meta?: LoggerMeta) => void 5export type LoggerHook = (message: LoggerMessage, meta?: LoggerMeta) => void
6export type LoggerLevel = 'info' | 'warn' | 'error' 6export type LoggerLevel = 'info' | 'warn' | 'error'
@@ -56,7 +56,7 @@ class Logger {
56 }) 56 })
57 57
58 try { 58 try {
59 const tokens = UserTokens.getUserTokens(peertubeLocalStorage) 59 const tokens = OAuthUserTokens.getUserTokens(peertubeLocalStorage)
60 60
61 if (tokens) headers.set('Authorization', `${tokens.tokenType} ${tokens.accessToken}`) 61 if (tokens) headers.set('Authorization', `${tokens.tokenType} ${tokens.accessToken}`)
62 } catch (err) { 62 } catch (err) {
diff --git a/client/src/root-helpers/users/index.ts b/client/src/root-helpers/users/index.ts
index 2b11d0b7e..c03e67325 100644
--- a/client/src/root-helpers/users/index.ts
+++ b/client/src/root-helpers/users/index.ts
@@ -1,2 +1,2 @@
1export * from './user-local-storage-keys' 1export * from './user-local-storage-keys'
2export * from './user-tokens' 2export * from './oauth-user-tokens'
diff --git a/client/src/root-helpers/users/user-tokens.ts b/client/src/root-helpers/users/oauth-user-tokens.ts
index a6d614cb7..a24e76b91 100644
--- a/client/src/root-helpers/users/user-tokens.ts
+++ b/client/src/root-helpers/users/oauth-user-tokens.ts
@@ -1,11 +1,11 @@
1import { UserTokenLocalStorageKeys } from './user-local-storage-keys' 1import { UserTokenLocalStorageKeys } from './user-local-storage-keys'
2 2
3export class UserTokens { 3export class OAuthUserTokens {
4 accessToken: string 4 accessToken: string
5 refreshToken: string 5 refreshToken: string
6 tokenType: string 6 tokenType: string
7 7
8 constructor (hash?: Partial<UserTokens>) { 8 constructor (hash?: Partial<OAuthUserTokens>) {
9 if (hash) { 9 if (hash) {
10 this.accessToken = hash.accessToken 10 this.accessToken = hash.accessToken
11 this.refreshToken = hash.refreshToken 11 this.refreshToken = hash.refreshToken
@@ -25,14 +25,14 @@ export class UserTokens {
25 25
26 if (!accessTokenLocalStorage || !refreshTokenLocalStorage || !tokenTypeLocalStorage) return null 26 if (!accessTokenLocalStorage || !refreshTokenLocalStorage || !tokenTypeLocalStorage) return null
27 27
28 return new UserTokens({ 28 return new OAuthUserTokens({
29 accessToken: accessTokenLocalStorage, 29 accessToken: accessTokenLocalStorage,
30 refreshToken: refreshTokenLocalStorage, 30 refreshToken: refreshTokenLocalStorage,
31 tokenType: tokenTypeLocalStorage 31 tokenType: tokenTypeLocalStorage
32 }) 32 })
33 } 33 }
34 34
35 static saveToLocalStorage (localStorage: Pick<Storage, 'setItem'>, tokens: UserTokens) { 35 static saveToLocalStorage (localStorage: Pick<Storage, 'setItem'>, tokens: OAuthUserTokens) {
36 localStorage.setItem(UserTokenLocalStorageKeys.ACCESS_TOKEN, tokens.accessToken) 36 localStorage.setItem(UserTokenLocalStorageKeys.ACCESS_TOKEN, tokens.accessToken)
37 localStorage.setItem(UserTokenLocalStorageKeys.REFRESH_TOKEN, tokens.refreshToken) 37 localStorage.setItem(UserTokenLocalStorageKeys.REFRESH_TOKEN, tokens.refreshToken)
38 localStorage.setItem(UserTokenLocalStorageKeys.TOKEN_TYPE, tokens.tokenType) 38 localStorage.setItem(UserTokenLocalStorageKeys.TOKEN_TYPE, tokens.tokenType)
diff --git a/client/src/root-helpers/video.ts b/client/src/root-helpers/video.ts
index ba84e49ea..107ba1eba 100644
--- a/client/src/root-helpers/video.ts
+++ b/client/src/root-helpers/video.ts
@@ -1,4 +1,4 @@
1import { HTMLServerConfig, Video } from '@shared/models' 1import { HTMLServerConfig, Video, VideoPrivacy } from '@shared/models'
2 2
3function buildVideoOrPlaylistEmbed (options: { 3function buildVideoOrPlaylistEmbed (options: {
4 embedUrl: string 4 embedUrl: string
@@ -26,9 +26,14 @@ function isP2PEnabled (video: Video, config: HTMLServerConfig, userP2PEnabled: b
26 return userP2PEnabled 26 return userP2PEnabled
27} 27}
28 28
29function videoRequiresAuth (video: Video) {
30 return new Set([ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]).has(video.privacy.id)
31}
32
29export { 33export {
30 buildVideoOrPlaylistEmbed, 34 buildVideoOrPlaylistEmbed,
31 isP2PEnabled 35 isP2PEnabled,
36 videoRequiresAuth
32} 37}
33 38
34// --------------------------------------------------------------------------- 39// ---------------------------------------------------------------------------
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index 451e54840..c6160151a 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -6,7 +6,7 @@ import { peertubeTranslate } from '../../../../shared/core-utils/i18n'
6import { HTMLServerConfig, LiveVideo, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement } from '../../../../shared/models' 6import { HTMLServerConfig, LiveVideo, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement } from '../../../../shared/models'
7import { PeertubePlayerManager } from '../../assets/player' 7import { PeertubePlayerManager } from '../../assets/player'
8import { TranslationsManager } from '../../assets/player/translations-manager' 8import { TranslationsManager } from '../../assets/player/translations-manager'
9import { getParamString, logger } from '../../root-helpers' 9import { getParamString, logger, videoRequiresAuth } from '../../root-helpers'
10import { PeerTubeEmbedApi } from './embed-api' 10import { PeerTubeEmbedApi } from './embed-api'
11import { AuthHTTP, LiveManager, PeerTubePlugin, PlayerManagerOptions, PlaylistFetcher, PlaylistTracker, VideoFetcher } from './shared' 11import { AuthHTTP, LiveManager, PeerTubePlugin, PlayerManagerOptions, PlaylistFetcher, PlaylistTracker, VideoFetcher } from './shared'
12import { PlayerHTML } from './shared/player-html' 12import { PlayerHTML } from './shared/player-html'
@@ -167,22 +167,25 @@ export class PeerTubeEmbed {
167 private async buildVideoPlayer (videoResponse: Response, captionsPromise: Promise<Response>) { 167 private async buildVideoPlayer (videoResponse: Response, captionsPromise: Promise<Response>) {
168 const alreadyHadPlayer = this.resetPlayerElement() 168 const alreadyHadPlayer = this.resetPlayerElement()
169 169
170 const videoInfoPromise: Promise<{ video: VideoDetails, live?: LiveVideo }> = videoResponse.json() 170 const videoInfoPromise = videoResponse.json()
171 .then((videoInfo: VideoDetails) => { 171 .then(async (videoInfo: VideoDetails) => {
172 this.playerManagerOptions.loadParams(this.config, videoInfo) 172 this.playerManagerOptions.loadParams(this.config, videoInfo)
173 173
174 if (!alreadyHadPlayer && !this.playerManagerOptions.hasAutoplay()) { 174 if (!alreadyHadPlayer && !this.playerManagerOptions.hasAutoplay()) {
175 this.playerHTML.buildPlaceholder(videoInfo) 175 this.playerHTML.buildPlaceholder(videoInfo)
176 } 176 }
177 const live = videoInfo.isLive
178 ? await this.videoFetcher.loadLive(videoInfo)
179 : undefined
177 180
178 if (!videoInfo.isLive) { 181 const videoFileToken = videoRequiresAuth(videoInfo)
179 return { video: videoInfo } 182 ? await this.videoFetcher.loadVideoToken(videoInfo)
180 } 183 : undefined
181 184
182 return this.videoFetcher.loadVideoWithLive(videoInfo) 185 return { live, video: videoInfo, videoFileToken }
183 }) 186 })
184 187
185 const [ { video, live }, translations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([ 188 const [ { video, live, videoFileToken }, translations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([
186 videoInfoPromise, 189 videoInfoPromise,
187 this.translationsPromise, 190 this.translationsPromise,
188 captionsPromise, 191 captionsPromise,
@@ -200,6 +203,9 @@ export class PeerTubeEmbed {
200 translations, 203 translations,
201 serverConfig: this.config, 204 serverConfig: this.config,
202 205
206 authorizationHeader: () => this.http.getHeaderTokenValue(),
207 videoFileToken: () => videoFileToken,
208
203 onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer(uuid), 209 onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer(uuid),
204 210
205 playlistTracker: this.playlistTracker, 211 playlistTracker: this.playlistTracker,
diff --git a/client/src/standalone/videos/shared/auth-http.ts b/client/src/standalone/videos/shared/auth-http.ts
index 0356ab8a6..43af5dff4 100644
--- a/client/src/standalone/videos/shared/auth-http.ts
+++ b/client/src/standalone/videos/shared/auth-http.ts
@@ -1,5 +1,5 @@
1import { HttpStatusCode, OAuth2ErrorCode, UserRefreshToken } from '../../../../../shared/models' 1import { HttpStatusCode, OAuth2ErrorCode, UserRefreshToken } from '../../../../../shared/models'
2import { objectToUrlEncoded, UserTokens } from '../../../root-helpers' 2import { OAuthUserTokens, objectToUrlEncoded } from '../../../root-helpers'
3import { peertubeLocalStorage } from '../../../root-helpers/peertube-web-storage' 3import { peertubeLocalStorage } from '../../../root-helpers/peertube-web-storage'
4 4
5export class AuthHTTP { 5export class AuthHTTP {
@@ -8,30 +8,30 @@ export class AuthHTTP {
8 CLIENT_SECRET: 'client_secret' 8 CLIENT_SECRET: 'client_secret'
9 } 9 }
10 10
11 private userTokens: UserTokens 11 private userOAuthTokens: OAuthUserTokens
12 12
13 private headers = new Headers() 13 private headers = new Headers()
14 14
15 constructor () { 15 constructor () {
16 this.userTokens = UserTokens.getUserTokens(peertubeLocalStorage) 16 this.userOAuthTokens = OAuthUserTokens.getUserTokens(peertubeLocalStorage)
17 17
18 if (this.userTokens) this.setHeadersFromTokens() 18 if (this.userOAuthTokens) this.setHeadersFromTokens()
19 } 19 }
20 20
21 fetch (url: string, { optionalAuth }: { optionalAuth: boolean }) { 21 fetch (url: string, { optionalAuth, method }: { optionalAuth: boolean, method?: string }) {
22 const refreshFetchOptions = optionalAuth 22 const refreshFetchOptions = optionalAuth
23 ? { headers: this.headers } 23 ? { headers: this.headers }
24 : {} 24 : {}
25 25
26 return this.refreshFetch(url.toString(), refreshFetchOptions) 26 return this.refreshFetch(url.toString(), { ...refreshFetchOptions, method })
27 } 27 }
28 28
29 getHeaderTokenValue () { 29 getHeaderTokenValue () {
30 return `${this.userTokens.tokenType} ${this.userTokens.accessToken}` 30 return `${this.userOAuthTokens.tokenType} ${this.userOAuthTokens.accessToken}`
31 } 31 }
32 32
33 isLoggedIn () { 33 isLoggedIn () {
34 return !!this.userTokens 34 return !!this.userOAuthTokens
35 } 35 }
36 36
37 private refreshFetch (url: string, options?: RequestInit) { 37 private refreshFetch (url: string, options?: RequestInit) {
@@ -47,7 +47,7 @@ export class AuthHTTP {
47 headers.set('Content-Type', 'application/x-www-form-urlencoded') 47 headers.set('Content-Type', 'application/x-www-form-urlencoded')
48 48
49 const data = { 49 const data = {
50 refresh_token: this.userTokens.refreshToken, 50 refresh_token: this.userOAuthTokens.refreshToken,
51 client_id: clientId, 51 client_id: clientId,
52 client_secret: clientSecret, 52 client_secret: clientSecret,
53 response_type: 'code', 53 response_type: 'code',
@@ -64,15 +64,15 @@ export class AuthHTTP {
64 return res.json() 64 return res.json()
65 }).then((obj: UserRefreshToken & { code?: OAuth2ErrorCode }) => { 65 }).then((obj: UserRefreshToken & { code?: OAuth2ErrorCode }) => {
66 if (!obj || obj.code === OAuth2ErrorCode.INVALID_GRANT) { 66 if (!obj || obj.code === OAuth2ErrorCode.INVALID_GRANT) {
67 UserTokens.flushLocalStorage(peertubeLocalStorage) 67 OAuthUserTokens.flushLocalStorage(peertubeLocalStorage)
68 this.removeTokensFromHeaders() 68 this.removeTokensFromHeaders()
69 69
70 return resolve() 70 return resolve()
71 } 71 }
72 72
73 this.userTokens.accessToken = obj.access_token 73 this.userOAuthTokens.accessToken = obj.access_token
74 this.userTokens.refreshToken = obj.refresh_token 74 this.userOAuthTokens.refreshToken = obj.refresh_token
75 UserTokens.saveToLocalStorage(peertubeLocalStorage, this.userTokens) 75 OAuthUserTokens.saveToLocalStorage(peertubeLocalStorage, this.userOAuthTokens)
76 76
77 this.setHeadersFromTokens() 77 this.setHeadersFromTokens()
78 78
@@ -84,7 +84,7 @@ export class AuthHTTP {
84 84
85 return refreshingTokenPromise 85 return refreshingTokenPromise
86 .catch(() => { 86 .catch(() => {
87 UserTokens.flushLocalStorage(peertubeLocalStorage) 87 OAuthUserTokens.flushLocalStorage(peertubeLocalStorage)
88 88
89 this.removeTokensFromHeaders() 89 this.removeTokensFromHeaders()
90 }).then(() => fetch(url, { 90 }).then(() => fetch(url, {
diff --git a/client/src/standalone/videos/shared/player-manager-options.ts b/client/src/standalone/videos/shared/player-manager-options.ts
index eed821994..87a84975b 100644
--- a/client/src/standalone/videos/shared/player-manager-options.ts
+++ b/client/src/standalone/videos/shared/player-manager-options.ts
@@ -17,7 +17,8 @@ import {
17 isP2PEnabled, 17 isP2PEnabled,
18 logger, 18 logger,
19 peertubeLocalStorage, 19 peertubeLocalStorage,
20 UserLocalStorageKeys 20 UserLocalStorageKeys,
21 videoRequiresAuth
21} from '../../../root-helpers' 22} from '../../../root-helpers'
22import { PeerTubePlugin } from './peertube-plugin' 23import { PeerTubePlugin } from './peertube-plugin'
23import { PlayerHTML } from './player-html' 24import { PlayerHTML } from './player-html'
@@ -154,6 +155,9 @@ export class PlayerManagerOptions {
154 captionsResponse: Response 155 captionsResponse: Response
155 live?: LiveVideo 156 live?: LiveVideo
156 157
158 authorizationHeader: () => string
159 videoFileToken: () => string
160
157 serverConfig: HTMLServerConfig 161 serverConfig: HTMLServerConfig
158 162
159 alreadyHadPlayer: boolean 163 alreadyHadPlayer: boolean
@@ -169,9 +173,11 @@ export class PlayerManagerOptions {
169 video, 173 video,
170 captionsResponse, 174 captionsResponse,
171 alreadyHadPlayer, 175 alreadyHadPlayer,
176 videoFileToken,
172 translations, 177 translations,
173 playlistTracker, 178 playlistTracker,
174 live, 179 live,
180 authorizationHeader,
175 serverConfig 181 serverConfig
176 } = options 182 } = options
177 183
@@ -227,6 +233,10 @@ export class PlayerManagerOptions {
227 embedUrl: window.location.origin + video.embedPath, 233 embedUrl: window.location.origin + video.embedPath,
228 embedTitle: video.name, 234 embedTitle: video.name,
229 235
236 requiresAuth: videoRequiresAuth(video),
237 authorizationHeader,
238 videoFileToken,
239
230 errorNotifier: () => { 240 errorNotifier: () => {
231 // Empty, we don't have a notifier in the embed 241 // Empty, we don't have a notifier in the embed
232 }, 242 },
diff --git a/client/src/standalone/videos/shared/video-fetcher.ts b/client/src/standalone/videos/shared/video-fetcher.ts
index b42d622f9..cf6d12831 100644
--- a/client/src/standalone/videos/shared/video-fetcher.ts
+++ b/client/src/standalone/videos/shared/video-fetcher.ts
@@ -1,4 +1,4 @@
1import { HttpStatusCode, LiveVideo, VideoDetails } from '../../../../../shared/models' 1import { HttpStatusCode, LiveVideo, VideoDetails, VideoToken } from '../../../../../shared/models'
2import { logger } from '../../../root-helpers' 2import { logger } from '../../../root-helpers'
3import { AuthHTTP } from './auth-http' 3import { AuthHTTP } from './auth-http'
4 4
@@ -36,10 +36,15 @@ export class VideoFetcher {
36 return { captionsPromise, videoResponse } 36 return { captionsPromise, videoResponse }
37 } 37 }
38 38
39 loadVideoWithLive (video: VideoDetails) { 39 loadLive (video: VideoDetails) {
40 return this.http.fetch(this.getLiveUrl(video.uuid), { optionalAuth: true }) 40 return this.http.fetch(this.getLiveUrl(video.uuid), { optionalAuth: true })
41 .then(res => res.json()) 41 .then(res => res.json() as Promise<LiveVideo>)
42 .then((live: LiveVideo) => ({ video, live })) 42 }
43
44 loadVideoToken (video: VideoDetails) {
45 return this.http.fetch(this.getVideoTokenUrl(video.uuid), { optionalAuth: true, method: 'POST' })
46 .then(res => res.json() as Promise<VideoToken>)
47 .then(token => token.files.token)
43 } 48 }
44 49
45 getVideoViewsUrl (videoUUID: string) { 50 getVideoViewsUrl (videoUUID: string) {
@@ -61,4 +66,8 @@ export class VideoFetcher {
61 private getLiveUrl (videoId: string) { 66 private getLiveUrl (videoId: string) {
62 return window.location.origin + '/api/v1/videos/live/' + videoId 67 return window.location.origin + '/api/v1/videos/live/' + videoId
63 } 68 }
69
70 private getVideoTokenUrl (id: string) {
71 return this.getVideoUrl(id) + '/token'
72 }
64} 73}