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 @@ -
+