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 2d13f1b58..cf9dc8f9c 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts @@ -20,6 +20,7 @@ import { environment } from '../../../environments/environment' import { VideoCaptionService } from '@app/shared/video-caption' import { MarkdownService } from '@app/shared/renderer' import { + CustomizationOptions, P2PMediaLoaderOptions, PeertubePlayerManager, PeertubePlayerManagerOptions, @@ -28,7 +29,7 @@ import { import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' import { Video } from '@app/shared/video/video.model' -import { isWebRTCDisabled } from '../../../assets/player/utils' +import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils' import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component' @Component({ @@ -249,8 +250,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy { const urlOptions = { startTime: queryParams.start, stopTime: queryParams.stop, + + muted: queryParams.muted, + loop: queryParams.loop, subtitle: queryParams.subtitle, - playerMode: queryParams.mode + + playerMode: queryParams.mode, + peertubeLink: false } this.onVideoFetched(video, captionsResult.data, urlOptions) @@ -327,7 +333,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { private async onVideoFetched ( video: VideoDetails, videoCaptions: VideoCaption[], - urlOptions: { startTime?: number, stopTime?: number, subtitle?: string, playerMode?: string } + urlOptions: CustomizationOptions & { playerMode: PlayerMode } ) { this.video = video @@ -339,7 +345,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { this.videoWatchPlaylist.updatePlaylistIndex(video) - let startTime = urlOptions.startTime || (this.video.userHistory ? this.video.userHistory.currentTime : 0) + let startTime = timeToInt(urlOptions.startTime) || (this.video.userHistory ? this.video.userHistory.currentTime : 0) // If we are at the end of the video, reset the timer if (this.video.duration - startTime <= 1) startTime = 0 @@ -378,12 +384,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy { enableHotkeys: true, inactivityTimeout: 2500, poster: this.video.previewUrl, + startTime, stopTime: urlOptions.stopTime, + controls: urlOptions.controls, + muted: urlOptions.muted, + loop: urlOptions.loop, + subtitle: urlOptions.subtitle, + + peertubeLink: urlOptions.peertubeLink, theaterMode: true, captions: videoCaptions.length !== 0, - peertubeLink: false, videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) @@ -392,8 +404,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { language: this.localeId, - subtitle: urlOptions.subtitle, - userWatching: this.user && this.user.videosHistoryEnabled === true ? { url: this.videoService.getUserWatchingVideoUrl(this.video.uuid), authorizationHeader: this.authService.getRequestHeaderValue() diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index 31cbc7dfd..8f6237326 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts @@ -39,7 +39,19 @@ export type P2PMediaLoaderOptions = { videoFiles: VideoFile[] } -export type CommonOptions = { +export interface CustomizationOptions { + startTime: number | string + stopTime: number | string + + controls?: boolean + muted?: boolean + loop?: boolean + subtitle?: string + + peertubeLink: boolean +} + +export interface CommonOptions extends CustomizationOptions { playerElement: HTMLVideoElement onPlayerElementChange: (element: HTMLVideoElement) => void @@ -48,21 +60,14 @@ export type CommonOptions = { enableHotkeys: boolean inactivityTimeout: number poster: string - startTime: number | string - stopTime: number | string theaterMode: boolean captions: boolean - peertubeLink: boolean videoViewUrl: string embedUrl: string language?: string - controls?: boolean - muted?: boolean - loop?: boolean - subtitle?: string videoCaptions: VideoJSCaption[] diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss index c7b205b11..aafeda257 100644 --- a/client/src/sass/include/_variables.scss +++ b/client/src/sass/include/_variables.scss @@ -71,7 +71,9 @@ $variables: ( --menuForegroundColor: var(--menuForegroundColor), --submenuColor: var(--submenuColor), --inputColor: var(--inputColor), - --inputPlaceholderColor: var(--inputPlaceholderColor) + --inputPlaceholderColor: var(--inputPlaceholderColor), + --embedForegroundColor: var(--embedForegroundColor), + --embedBigPlayBackgroundColor: var(--embedBigPlayBackgroundColor) ); /*** theme helper ***/ diff --git a/client/src/sass/player/_player-variables.scss b/client/src/sass/player/_player-variables.scss index 110129790..4e9e8736c 100644 --- a/client/src/sass/player/_player-variables.scss +++ b/client/src/sass/player/_player-variables.scss @@ -10,4 +10,10 @@ $slider-bg-color: lighten($primary-background-color, 33%); $progress-margin: 10px; -$assets-path: '../../assets/' !default; \ No newline at end of file +$assets-path: '../../assets/' !default; + +body { + --embedForegroundColor: #{$primary-foreground-color}; + + --embedBigPlayBackgroundColor: #{$primary-background-color}; +} diff --git a/client/src/sass/player/context-menu.scss b/client/src/sass/player/context-menu.scss index 71d6d1b1d..eeab0ccdf 100644 --- a/client/src/sass/player/context-menu.scss +++ b/client/src/sass/player/context-menu.scss @@ -14,7 +14,7 @@ $context-menu-width: 350px; .vjs-menu-content { opacity: $primary-foreground-opacity; - color: $primary-foreground-color; + color: var(--embedForegroundCsolor); font-size: $font-size !important; font-weight: $font-semibold; } @@ -30,4 +30,4 @@ $context-menu-width: 350px; background-color: rgba(255, 255, 255, 0.2); } } -} \ No newline at end of file +} diff --git a/client/src/sass/player/peertube-skin.scss b/client/src/sass/player/peertube-skin.scss index e63a2875c..996024ade 100644 --- a/client/src/sass/player/peertube-skin.scss +++ b/client/src/sass/player/peertube-skin.scss @@ -10,9 +10,8 @@ } .video-js.vjs-peertube-skin { - font-size: $font-size; - color: $primary-foreground-color; + color: var(--embedForegroundColor); .vjs-dock-text { padding-right: 10px; @@ -114,7 +113,7 @@ .vjs-control-bar, .vjs-big-play-button, .vjs-settings-dialog { - background-color: rgba($primary-background-color, 0.5); + background-color: var(--embedBigPlayBackgroundColor); } .vjs-poster { @@ -139,7 +138,8 @@ .vjs-theater-control, .vjs-settings { - color: $primary-foreground-color !important; + color: var(--embedForegroundColor) !important; + opacity: $primary-foreground-opacity; transition: opacity .1s; @@ -151,7 +151,7 @@ .vjs-current-time, .vjs-duration, .vjs-peertube { - color: $primary-foreground-color; + color: var(--embedForegroundColor); opacity: $primary-foreground-opacity; } @@ -171,7 +171,7 @@ transition: none; .vjs-play-progress { - background: $primary-foreground-color; + background: var(--embedForegroundColor); // Not display the circle if the progress is not hovered &::before { diff --git a/client/src/sass/player/settings-menu.scss b/client/src/sass/player/settings-menu.scss index 61965c85e..83407b445 100644 --- a/client/src/sass/player/settings-menu.scss +++ b/client/src/sass/player/settings-menu.scss @@ -38,7 +38,7 @@ $setting-transition-easing: ease-out; position: absolute; right: .5em; bottom: 3.5em; - color: $primary-foreground-color; + color: var(--embedForegroundColor); opacity: $primary-foreground-opacity; margin: 0 auto; font-size: $font-size !important; diff --git a/client/src/standalone/videos/embed-api.ts b/client/src/standalone/videos/embed-api.ts new file mode 100644 index 000000000..169e371da --- /dev/null +++ b/client/src/standalone/videos/embed-api.ts @@ -0,0 +1,130 @@ +import './embed.scss' + +import * as Channel from 'jschannel' +import { PeerTubeResolution } from '../player/definitions' +import { PeerTubeEmbed } from './embed' + +/** + * Embed API exposes control of the embed player to the outside world via + * JSChannels and window.postMessage + */ +export class PeerTubeEmbedApi { + private channel: Channel.MessagingChannel + private isReady = false + private resolutions: PeerTubeResolution[] = null + + constructor (private embed: PeerTubeEmbed) { + } + + initialize () { + this.constructChannel() + this.setupStateTracking() + + // We're ready! + + this.notifyReady() + } + + private get element () { + return this.embed.videoElement + } + + private constructChannel () { + const channel = Channel.build({ window: window.parent, origin: '*', scope: this.embed.scope }) + + channel.bind('play', (txn, params) => this.embed.player.play()) + channel.bind('pause', (txn, params) => this.embed.player.pause()) + channel.bind('seek', (txn, time) => this.embed.player.currentTime(time)) + channel.bind('setVolume', (txn, value) => this.embed.player.volume(value)) + channel.bind('getVolume', (txn, value) => this.embed.player.volume()) + channel.bind('isReady', (txn, params) => this.isReady) + channel.bind('setResolution', (txn, resolutionId) => this.setResolution(resolutionId)) + channel.bind('getResolutions', (txn, params) => this.resolutions) + channel.bind('setPlaybackRate', (txn, playbackRate) => this.embed.player.playbackRate(playbackRate)) + channel.bind('getPlaybackRate', (txn, params) => this.embed.player.playbackRate()) + channel.bind('getPlaybackRates', (txn, params) => this.embed.playerOptions.playbackRates) + + this.channel = channel + } + + private setResolution (resolutionId: number) { + if (resolutionId === -1 && this.embed.player.webtorrent().isAutoResolutionForbidden()) return + + // Auto resolution + if (resolutionId === -1) { + this.embed.player.webtorrent().enableAutoResolution() + return + } + + this.embed.player.webtorrent().disableAutoResolution() + this.embed.player.webtorrent().updateResolution(resolutionId) + } + + /** + * Let the host know that we're ready to go! + */ + private notifyReady () { + this.isReady = true + this.channel.notify({ method: 'ready', params: true }) + } + + private setupStateTracking () { + let currentState: 'playing' | 'paused' | 'unstarted' = 'unstarted' + + setInterval(() => { + const position = this.element.currentTime + const volume = this.element.volume + + this.channel.notify({ + method: 'playbackStatusUpdate', + params: { + position, + volume, + playbackState: currentState + } + }) + }, 500) + + this.element.addEventListener('play', ev => { + currentState = 'playing' + this.channel.notify({ method: 'playbackStatusChange', params: 'playing' }) + }) + + this.element.addEventListener('pause', ev => { + currentState = 'paused' + this.channel.notify({ method: 'playbackStatusChange', params: 'paused' }) + }) + + // PeerTube specific capabilities + + if (this.embed.player.webtorrent) { + this.embed.player.webtorrent().on('autoResolutionUpdate', () => this.loadWebTorrentResolutions()) + this.embed.player.webtorrent().on('videoFileUpdate', () => this.loadWebTorrentResolutions()) + } + } + + private loadWebTorrentResolutions () { + const resolutions = [] + const currentResolutionId = this.embed.player.webtorrent().getCurrentResolutionId() + + for (const videoFile of this.embed.player.webtorrent().videoFiles) { + let label = videoFile.resolution.label + if (videoFile.fps && videoFile.fps >= 50) { + label += videoFile.fps + } + + resolutions.push({ + id: videoFile.resolution.id, + label, + src: videoFile.magnetUri, + active: videoFile.resolution.id === currentResolutionId + }) + } + + this.resolutions = resolutions + this.channel.notify({ + method: 'resolutionUpdate', + params: this.resolutions + }) + } +} diff --git a/client/src/standalone/videos/embed.html b/client/src/standalone/videos/embed.html index c3b6e08ca..5a15bf552 100644 --- a/client/src/standalone/videos/embed.html +++ b/client/src/standalone/videos/embed.html @@ -11,7 +11,7 @@ - +

diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index 707f04253..cfe8e94b1 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts @@ -1,9 +1,6 @@ import './embed.scss' -import * as Channel from 'jschannel' - import { peertubeTranslate, ResultList, ServerConfig, VideoDetails } from '../../../../shared' -import { PeerTubeResolution } from '../player/definitions' import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' import { @@ -13,133 +10,9 @@ import { PlayerMode } from '../../assets/player/peertube-player-manager' import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' +import { PeerTubeEmbedApi } from './embed-api' -/** - * Embed API exposes control of the embed player to the outside world via - * JSChannels and window.postMessage - */ -class PeerTubeEmbedApi { - private channel: Channel.MessagingChannel - private isReady = false - private resolutions: PeerTubeResolution[] = null - - constructor (private embed: PeerTubeEmbed) { - } - - initialize () { - this.constructChannel() - this.setupStateTracking() - - // We're ready! - - this.notifyReady() - } - - private get element () { - return this.embed.videoElement - } - - private constructChannel () { - const channel = Channel.build({ window: window.parent, origin: '*', scope: this.embed.scope }) - - channel.bind('play', (txn, params) => this.embed.player.play()) - channel.bind('pause', (txn, params) => this.embed.player.pause()) - channel.bind('seek', (txn, time) => this.embed.player.currentTime(time)) - channel.bind('setVolume', (txn, value) => this.embed.player.volume(value)) - channel.bind('getVolume', (txn, value) => this.embed.player.volume()) - channel.bind('isReady', (txn, params) => this.isReady) - channel.bind('setResolution', (txn, resolutionId) => this.setResolution(resolutionId)) - channel.bind('getResolutions', (txn, params) => this.resolutions) - channel.bind('setPlaybackRate', (txn, playbackRate) => this.embed.player.playbackRate(playbackRate)) - channel.bind('getPlaybackRate', (txn, params) => this.embed.player.playbackRate()) - channel.bind('getPlaybackRates', (txn, params) => this.embed.playerOptions.playbackRates) - - this.channel = channel - } - - private setResolution (resolutionId: number) { - if (resolutionId === -1 && this.embed.player.webtorrent().isAutoResolutionForbidden()) return - - // Auto resolution - if (resolutionId === -1) { - this.embed.player.webtorrent().enableAutoResolution() - return - } - - this.embed.player.webtorrent().disableAutoResolution() - this.embed.player.webtorrent().updateResolution(resolutionId) - } - - /** - * Let the host know that we're ready to go! - */ - private notifyReady () { - this.isReady = true - this.channel.notify({ method: 'ready', params: true }) - } - - private setupStateTracking () { - let currentState: 'playing' | 'paused' | 'unstarted' = 'unstarted' - - setInterval(() => { - const position = this.element.currentTime - const volume = this.element.volume - - this.channel.notify({ - method: 'playbackStatusUpdate', - params: { - position, - volume, - playbackState: currentState - } - }) - }, 500) - - this.element.addEventListener('play', ev => { - currentState = 'playing' - this.channel.notify({ method: 'playbackStatusChange', params: 'playing' }) - }) - - this.element.addEventListener('pause', ev => { - currentState = 'paused' - this.channel.notify({ method: 'playbackStatusChange', params: 'paused' }) - }) - - // PeerTube specific capabilities - - if (this.embed.player.webtorrent) { - this.embed.player.webtorrent().on('autoResolutionUpdate', () => this.loadWebTorrentResolutions()) - this.embed.player.webtorrent().on('videoFileUpdate', () => this.loadWebTorrentResolutions()) - } - } - - private loadWebTorrentResolutions () { - const resolutions = [] - const currentResolutionId = this.embed.player.webtorrent().getCurrentResolutionId() - - for (const videoFile of this.embed.player.webtorrent().videoFiles) { - let label = videoFile.resolution.label - if (videoFile.fps && videoFile.fps >= 50) { - label += videoFile.fps - } - - resolutions.push({ - id: videoFile.resolution.id, - label, - src: videoFile.magnetUri, - active: videoFile.resolution.id === currentResolutionId - }) - } - - this.resolutions = resolutions - this.channel.notify({ - method: 'resolutionUpdate', - params: this.resolutions - }) - } -} - -class PeerTubeEmbed { +export class PeerTubeEmbed { videoElement: HTMLVideoElement player: any playerOptions: any @@ -152,6 +25,12 @@ class PeerTubeEmbed { enableApi = false startTime: number | string = 0 stopTime: number | string + + title: boolean + warningTitle: boolean + bigPlayBackgroundColor: string + foregroundColor: string + mode: PlayerMode scope = 'peertube' @@ -245,13 +124,18 @@ class PeerTubeEmbed { this.controls = this.getParamToggle(params, 'controls', true) this.muted = this.getParamToggle(params, 'muted', false) this.loop = this.getParamToggle(params, 'loop', false) + this.title = this.getParamToggle(params, 'title', true) this.enableApi = this.getParamToggle(params, 'api', this.enableApi) + this.warningTitle = this.getParamToggle(params, 'warningTitle', true) this.scope = this.getParamString(params, 'scope', this.scope) this.subtitle = this.getParamString(params, 'subtitle') this.startTime = this.getParamString(params, 'start') this.stopTime = this.getParamString(params, 'stop') + this.bigPlayBackgroundColor = this.getParamString(params, 'bigPlayBackgroundColor') + this.foregroundColor = this.getParamString(params, 'foregroundColor') + this.mode = this.getParamString(params, 'mode') === 'p2p-media-loader' ? 'p2p-media-loader' : 'webtorrent' } catch (err) { console.error('Cannot get params from URL.', err) @@ -276,15 +160,7 @@ class PeerTubeEmbed { } const videoInfo: VideoDetails = await videoResponse.json() - let videoCaptions: VideoJSCaption[] = [] - if (captionsResponse.ok) { - const { data } = (await captionsResponse.json()) as ResultList - videoCaptions = data.map(c => ({ - label: peertubeTranslate(c.language.label, serverTranslations), - language: c.language.id, - src: window.location.origin + c.captionPath - })) - } + const videoCaptions = await this.buildCaptions(serverTranslations, captionsResponse) this.loadParams() @@ -337,22 +213,13 @@ class PeerTubeEmbed { } this.player = await PeertubePlayerManager.initialize(this.mode, options) - this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations)) window[ 'videojsPlayer' ] = this.player - if (this.controls) { - const config: ServerConfig = await configResponse.json() - const description = config.tracker.enabled - ? '' + this.player.localize('Uses P2P, others may know your IP is downloading this video.') + '' - : undefined + this.buildCSS() - this.player.dock({ - title: videoInfo.name, - description - }) - } + await this.buildDock(videoInfo, configResponse) this.initializeApi() } @@ -365,6 +232,48 @@ class PeerTubeEmbed { return } } + + private async buildDock (videoInfo: VideoDetails, configResponse: Response) { + if (this.controls) { + const title = this.title ? videoInfo.name : undefined + + const config: ServerConfig = await configResponse.json() + const description = config.tracker.enabled && this.warningTitle + ? '' + this.player.localize('Uses P2P, others may know your IP is downloading this video.') + '' + : undefined + + this.player.dock({ + title, + description + }) + } + } + + private buildCSS () { + const body = document.getElementById('custom-css') + + if (this.bigPlayBackgroundColor) { + body.style.setProperty('--embedBigPlayBackgroundColor', this.bigPlayBackgroundColor) + } + + if (this.foregroundColor) { + body.style.setProperty('--embedForegroundColor', this.foregroundColor) + } + } + + private async buildCaptions (serverTranslations: any, captionsResponse: Response): Promise { + if (captionsResponse.ok) { + const { data } = (await captionsResponse.json()) as ResultList + + return data.map(c => ({ + label: peertubeTranslate(c.language.label, serverTranslations), + language: c.language.id, + src: window.location.origin + c.captionPath + })) + } + + return [] + } } PeerTubeEmbed.main() diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index 3268f8c90..2316033a1 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts @@ -379,8 +379,7 @@ describe('Test users API validators', function () { it('Should succeed without password change with the correct params', async function () { const fields = { nsfwPolicy: 'blur', - autoPlayVideo: false, - email: 'super_email@example.com' + autoPlayVideo: false } await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields, statusCodeExpected: 204 }) diff --git a/server/tests/api/users/users-verification.ts b/server/tests/api/users/users-verification.ts index b8fa1430b..7cd61f539 100644 --- a/server/tests/api/users/users-verification.ts +++ b/server/tests/api/users/users-verification.ts @@ -109,7 +109,8 @@ describe('Test users account verification', function () { await updateMyUser({ url: server.url, accessToken: userAccessToken, - email: 'updated@example.com' + email: 'updated@example.com', + currentPassword: user1.password }) await waitJobs(server)