diff options
Diffstat (limited to 'client/src/standalone')
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 @@ | |||
1 | import './embed.scss' | 1 | import './embed.scss' |
2 | import * as Channel from 'jschannel' | 2 | import * as Channel from 'jschannel' |
3 | import { logger } from '../../root-helpers' | 3 | import { logger } from '../../root-helpers' |
4 | import { PeerTubeResolution, PeerTubeTextTrack } from '../player/definitions' | 4 | import { PeerTubeResolution, PeerTubeTextTrack } from '../embed-player-api/definitions' |
5 | import { PeerTubeEmbed } from './embed' | 5 | import { 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, | |||
24 | body { | 24 | body { |
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 @@ | |||
1 | import './embed.scss' | 1 | import './embed.scss' |
2 | import '../../assets/player/shared/dock/peertube-dock-component' | 2 | import '../../assets/player/shared/dock/peertube-dock-component' |
3 | import '../../assets/player/shared/dock/peertube-dock-plugin' | 3 | import '../../assets/player/shared/dock/peertube-dock-plugin' |
4 | import { PeerTubeServerError } from 'src/types' | ||
4 | import videojs from 'video.js' | 5 | import videojs from 'video.js' |
5 | import { peertubeTranslate } from '../../../../shared/core-utils/i18n' | 6 | import { |
6 | import { HTMLServerConfig, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement, VideoState } from '../../../../shared/models' | 7 | HTMLServerConfig, |
7 | import { PeertubePlayerManager } from '../../assets/player' | 8 | ResultList, |
9 | ServerErrorCode, | ||
10 | VideoDetails, | ||
11 | VideoPlaylist, | ||
12 | VideoPlaylistElement, | ||
13 | VideoState | ||
14 | } from '../../../../shared/models' | ||
15 | import { PeerTubePlayer } from '../../assets/player/peertube-player' | ||
8 | import { TranslationsManager } from '../../assets/player/translations-manager' | 16 | import { TranslationsManager } from '../../assets/player/translations-manager' |
9 | import { getParamString, logger, videoRequiresAuth } from '../../root-helpers' | 17 | import { getParamString, logger, videoRequiresFileToken } from '../../root-helpers' |
10 | import { PeerTubeEmbedApi } from './embed-api' | 18 | import { PeerTubeEmbedApi } from './embed-api' |
11 | import { | 19 | import { |
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 | ||
406 | PeerTubeEmbed.main() | 394 | PeerTubeEmbed.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' | |||
2 | export * from './peertube-plugin' | 2 | export * from './peertube-plugin' |
3 | export * from './live-manager' | 3 | export * from './live-manager' |
4 | export * from './player-html' | 4 | export * from './player-html' |
5 | export * from './player-manager-options' | 5 | export * from './player-options-builder' |
6 | export * from './playlist-fetcher' | 6 | export * from './playlist-fetcher' |
7 | export * from './playlist-tracker' | 7 | export * from './playlist-tracker' |
8 | export * from './translations' | 8 | export * 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 @@ | |||
1 | import { peertubeTranslate } from '../../../../../shared/core-utils/i18n' | 1 | import { peertubeTranslate } from '../../../../../shared/core-utils/i18n' |
2 | import { VideoDetails } from '../../../../../shared/models' | ||
3 | import { logger } from '../../../root-helpers' | 2 | import { logger } from '../../../root-helpers' |
4 | import { Translations } from './translations' | 3 | import { 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' | |||
2 | import { | 2 | import { |
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' |
12 | import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode, VideoJSCaption } from '../../../assets/player' | 13 | import { HLSOptions, PeerTubePlayerContructorOptions, PeerTubePlayerLoadOptions, PlayerMode, VideoJSCaption } from '../../../assets/player' |
13 | import { | 14 | import { |
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' |
23 | import { PeerTubePlugin } from './peertube-plugin' | 24 | import { PeerTubePlugin } from './peertube-plugin' |
24 | import { PlayerHTML } from './player-html' | 25 | import { PlayerHTML } from './player-html' |
@@ -26,7 +27,7 @@ import { PlaylistTracker } from './playlist-tracker' | |||
26 | import { Translations } from './translations' | 27 | import { Translations } from './translations' |
27 | import { VideoFetcher } from './video-fetcher' | 28 | import { VideoFetcher } from './video-fetcher' |
28 | 29 | ||
29 | export class PlayerManagerOptions { | 30 | export 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 @@ | |||
1 | import { HttpStatusCode, LiveVideo, VideoDetails, VideoToken } from '../../../../../shared/models' | 1 | import { HttpStatusCode, LiveVideo, VideoDetails, VideoToken } from '../../../../../shared/models' |
2 | import { logger } from '../../../root-helpers' | 2 | import { logger } from '../../../root-helpers' |
3 | import { PeerTubeServerError } from '../../../types' | ||
3 | import { AuthHTTP } from './auth-http' | 4 | import { AuthHTTP } from './auth-http' |
4 | 5 | ||
5 | export class VideoFetcher { | 6 | export 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 @@ | |||
1 | import './test-embed.scss' | 1 | import './test-embed.scss' |
2 | import { PeerTubeResolution, PlayerEventType } from '../player/definitions' | 2 | import { PeerTubeResolution, PlayerEventType } from '../embed-player-api/definitions' |
3 | import { PeerTubePlayer } from '../player/player' | 3 | import { PeerTubePlayer } from '../embed-player-api/player' |
4 | import { logger } from '../../root-helpers' | 4 | import { logger } from '../../root-helpers' |
5 | 5 | ||
6 | window.addEventListener('load', async () => { | 6 | window.addEventListener('load', async () => { |