From 2adfc7ea9a1f858db874df9fe322e7ae833db77c Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 23 Jan 2019 15:36:45 +0100 Subject: [PATCH] Refractor videojs player Add fake p2p-media-loader plugin --- client/package.json | 3 + .../+video-watch/video-watch.component.ts | 77 ++-- .../assets/player/p2p-media-loader-plugin.ts | 33 ++ .../assets/player/peertube-player-manager.ts | 388 ++++++++++++++++++ client/src/assets/player/peertube-player.ts | 300 -------------- client/src/assets/player/peertube-plugin.ts | 219 ++++++++++ .../assets/player/peertube-videojs-typings.ts | 67 ++- .../src/assets/player/resolution-menu-item.ts | 67 --- .../p2p-info-button.ts} | 10 +- .../peertube-link-button.ts | 4 +- .../peertube-load-progress-bar.ts | 4 +- .../resolution-menu-button.ts | 70 ++-- .../resolution-menu-item.ts | 87 ++++ .../settings-menu-button.ts | 4 +- .../settings-menu-item.ts | 11 +- .../theater-button.ts | 4 +- ...videojs-plugin.ts => webtorrent-plugin.ts} | 274 ++++--------- .../{ => webtorrent}/peertube-chunk-store.ts | 0 .../player/{ => webtorrent}/video-renderer.ts | 0 client/src/standalone/videos/embed.ts | 108 ++--- client/src/tsconfig.app.json | 2 +- client/yarn.lock | 51 ++- scripts/dev/server.sh | 2 +- server/middlewares/csp.ts | 2 +- 24 files changed, 1065 insertions(+), 722 deletions(-) create mode 100644 client/src/assets/player/p2p-media-loader-plugin.ts create mode 100644 client/src/assets/player/peertube-player-manager.ts delete mode 100644 client/src/assets/player/peertube-player.ts create mode 100644 client/src/assets/player/peertube-plugin.ts delete mode 100644 client/src/assets/player/resolution-menu-item.ts rename client/src/assets/player/{webtorrent-info-button.ts => videojs-components/p2p-info-button.ts} (91%) rename client/src/assets/player/{ => videojs-components}/peertube-link-button.ts (87%) rename client/src/assets/player/{ => videojs-components}/peertube-load-progress-bar.ts (85%) rename client/src/assets/player/{ => videojs-components}/resolution-menu-button.ts (56%) create mode 100644 client/src/assets/player/videojs-components/resolution-menu-item.ts rename client/src/assets/player/{ => videojs-components}/settings-menu-button.ts (98%) rename client/src/assets/player/{ => videojs-components}/settings-menu-item.ts (97%) rename client/src/assets/player/{ => videojs-components}/theater-button.ts (87%) rename client/src/assets/player/{peertube-videojs-plugin.ts => webtorrent-plugin.ts} (73%) rename client/src/assets/player/{ => webtorrent}/peertube-chunk-store.ts (100%) rename client/src/assets/player/{ => webtorrent}/video-renderer.ts (100%) diff --git a/client/package.json b/client/package.json index 31fc77887..9da7c1025 100644 --- a/client/package.json +++ b/client/package.json @@ -85,6 +85,7 @@ "@ngx-loading-bar/router": "^3.0.0", "@ngx-meta/core": "^6.0.0-rc.1", "@ngx-translate/i18n-polyfill": "^1.0.0", + "@streamroot/videojs-hlsjs-plugin": "^1.0.7", "@types/core-js": "^2.5.0", "@types/jasmine": "^2.8.7", "@types/jasminewd2": "^2.0.3", @@ -131,6 +132,7 @@ "ngx-qrcode2": "^0.0.9", "node-sass": "^4.9.3", "npm-font-source-sans-pro": "^1.0.2", + "p2p-media-loader-hlsjs": "^0.3.0", "path-browserify": "^1.0.0", "primeng": "^7.0.0", "process": "^0.11.10", @@ -152,6 +154,7 @@ "typescript": "3.1.6", "video.js": "^7", "videojs-contextmenu-ui": "^5.0.0", + "videojs-contrib-quality-levels": "^2.0.9", "videojs-dock": "^2.0.2", "videojs-hotkeys": "^0.2.21", "webpack-bundle-analyzer": "^3.0.2", 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 ee504bc58..6e38af195 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts @@ -7,14 +7,9 @@ import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-supp import { MetaService } from '@ngx-meta/core' import { Notifier, ServerService } from '@app/core' import { forkJoin, Subscription } from 'rxjs' -// FIXME: something weird with our path definition in tsconfig and typings -// @ts-ignore -import videojs from 'video.js' -import 'videojs-hotkeys' import { Hotkey, HotkeysService } from 'angular2-hotkeys' import * as WebTorrent from 'webtorrent' import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared' -import '../../../assets/player/peertube-videojs-plugin' import { AuthService, ConfirmService } from '../../core' import { RestExtractor, VideoBlacklistService } from '../../shared' import { VideoDetails } from '../../shared/video/video-details.model' @@ -24,12 +19,11 @@ import { VideoReportComponent } from './modal/video-report.component' import { VideoShareComponent } from './modal/video-share.component' import { VideoBlacklistComponent } from './modal/video-blacklist.component' import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component' -import { addContextMenu, getVideojsOptions, loadLocaleInVideoJS } from '../../../assets/player/peertube-player' import { I18n } from '@ngx-translate/i18n-polyfill' import { environment } from '../../../environments/environment' -import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' import { VideoCaptionService } from '@app/shared/video-caption' import { MarkdownService } from '@app/shared/renderer' +import { PeertubePlayerManager } from '../../../assets/player/peertube-player-manager' @Component({ selector: 'my-video-watch', @@ -46,7 +40,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent - player: videojs.Player + player: any playerElement: HTMLVideoElement userRating: UserVideoRateType = null video: VideoDetails = null @@ -61,7 +55,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { remoteServerDown = false hotkeys: Hotkey[] - private videojsLocaleLoaded = false private paramsSub: Subscription constructor ( @@ -402,41 +395,45 @@ export class VideoWatchComponent implements OnInit, OnDestroy { src: environment.apiUrl + c.captionPath })) - const videojsOptions = getVideojsOptions({ - autoplay: this.isAutoplay(), - inactivityTimeout: 2500, - videoFiles: this.video.files, - videoCaptions: playerCaptions, - playerElement: this.playerElement, - videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null, - videoDuration: this.video.duration, - enableHotkeys: true, - peertubeLink: false, - poster: this.video.previewUrl, - startTime, - subtitle: urlOptions.subtitle, - theaterMode: true, - language: this.localeId, - - userWatching: this.user && this.user.videosHistoryEnabled === true ? { - url: this.videoService.getUserWatchingVideoUrl(this.video.uuid), - authorizationHeader: this.authService.getRequestHeaderValue() - } : undefined - }) + const options = { + common: { + autoplay: this.isAutoplay(), + playerElement: this.playerElement, + videoDuration: this.video.duration, + enableHotkeys: true, + inactivityTimeout: 2500, + poster: this.video.previewUrl, + startTime, + + theaterMode: true, + captions: videoCaptions.length !== 0, + peertubeLink: false, + + videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null, + embedUrl: this.video.embedUrl, + + language: this.localeId, + + subtitle: urlOptions.subtitle, - if (this.videojsLocaleLoaded === false) { - await loadLocaleInVideoJS(environment.apiUrl, videojs, isOnDevLocale() ? getDevLocale() : this.localeId) - this.videojsLocaleLoaded = true + userWatching: this.user && this.user.videosHistoryEnabled === true ? { + url: this.videoService.getUserWatchingVideoUrl(this.video.uuid), + authorizationHeader: this.authService.getRequestHeaderValue() + } : undefined, + + serverUrl: environment.apiUrl, + + videoCaptions: playerCaptions + }, + + webtorrent: { + videoFiles: this.video.files + } } - const self = this this.zone.runOutsideAngular(async () => { - videojs(this.playerElement, videojsOptions, function (this: videojs.Player) { - self.player = this - this.on('customError', ({ err }: { err: any }) => self.handleError(err)) - - addContextMenu(self.player, self.video.embedUrl) - }) + this.player = await PeertubePlayerManager.initialize('webtorrent', options) + this.player.on('customError', ({ err }: { err: any }) => this.handleError(err)) }) this.setVideoDescriptionHTML() diff --git a/client/src/assets/player/p2p-media-loader-plugin.ts b/client/src/assets/player/p2p-media-loader-plugin.ts new file mode 100644 index 000000000..6d07a2c9c --- /dev/null +++ b/client/src/assets/player/p2p-media-loader-plugin.ts @@ -0,0 +1,33 @@ +// FIXME: something weird with our path definition in tsconfig and typings +// @ts-ignore +import * as videojs from 'video.js' +import { P2PMediaLoaderPluginOptions, VideoJSComponentInterface } from './peertube-videojs-typings' + +// videojs-hlsjs-plugin needs videojs in window +window['videojs'] = videojs +import '@streamroot/videojs-hlsjs-plugin' + +import { initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs' + +// import { Events } from '../p2p-media-loader/p2p-media-loader-core/lib' + +const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') +class P2pMediaLoaderPlugin extends Plugin { + + constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) { + super(player, options) + + initVideoJsContribHlsJsPlayer(player) + + console.log(options) + + player.src({ + type: options.type, + src: options.src + }) + } + +} + +videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin) +export { P2pMediaLoaderPlugin } diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts new file mode 100644 index 000000000..9155c0698 --- /dev/null +++ b/client/src/assets/player/peertube-player-manager.ts @@ -0,0 +1,388 @@ +import { VideoFile } from '../../../../shared/models/videos' +// @ts-ignore +import * as videojs from 'video.js' +import 'videojs-hotkeys' +import 'videojs-dock' +import 'videojs-contextmenu-ui' +import 'videojs-contrib-quality-levels' +import './peertube-plugin' +import './videojs-components/peertube-link-button' +import './videojs-components/resolution-menu-button' +import './videojs-components/settings-menu-button' +import './videojs-components/p2p-info-button' +import './videojs-components/peertube-load-progress-bar' +import './videojs-components/theater-button' +import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions, videojsUntyped } from './peertube-videojs-typings' +import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils' +import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n' +import { Engine } from 'p2p-media-loader-hlsjs' + +// Change 'Playback Rate' to 'Speed' (smaller for our settings menu) +videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' +// Change Captions to Subtitles/CC +videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC' +// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) +videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' ' + +type PlayerMode = 'webtorrent' | 'p2p-media-loader' + +type WebtorrentOptions = { + videoFiles: VideoFile[] +} + +type P2PMediaLoaderOptions = { + playlistUrl: string +} + +type CommonOptions = { + playerElement: HTMLVideoElement + + autoplay: boolean + videoDuration: number + enableHotkeys: boolean + inactivityTimeout: number + poster: string + startTime: number | string + + theaterMode: boolean + captions: boolean + peertubeLink: boolean + + videoViewUrl: string + embedUrl: string + + language?: string + controls?: boolean + muted?: boolean + loop?: boolean + subtitle?: string + + videoCaptions: VideoJSCaption[] + + userWatching?: UserWatching + + serverUrl: string +} + +export type PeertubePlayerManagerOptions = { + common: CommonOptions, + webtorrent?: WebtorrentOptions, + p2pMediaLoader?: P2PMediaLoaderOptions +} + +export class PeertubePlayerManager { + + private static videojsLocaleCache: { [ path: string ]: any } = {} + + static getServerTranslations (serverUrl: string, locale: string) { + const path = PeertubePlayerManager.getLocalePath(serverUrl, locale) + // It is the default locale, nothing to translate + if (!path) return Promise.resolve(undefined) + + return fetch(path + '/server.json') + .then(res => res.json()) + .catch(err => { + console.error('Cannot get server translations', err) + return undefined + }) + } + + static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions) { + if (mode === 'webtorrent') await import('./webtorrent-plugin') + if (mode === 'p2p-media-loader') await import('./p2p-media-loader-plugin') + + const videojsOptions = this.getVideojsOptions(mode, options) + + await this.loadLocaleInVideoJS(options.common.serverUrl, options.common.language) + + const self = this + return new Promise(res => { + videojs(options.common.playerElement, videojsOptions, function (this: any) { + const player = this + + self.addContextMenu(mode, player, options.common.embedUrl) + + return res(player) + }) + }) + } + + private static loadLocaleInVideoJS (serverUrl: string, locale: string) { + const path = PeertubePlayerManager.getLocalePath(serverUrl, locale) + // It is the default locale, nothing to translate + if (!path) return Promise.resolve(undefined) + + let p: Promise + + if (PeertubePlayerManager.videojsLocaleCache[path]) { + p = Promise.resolve(PeertubePlayerManager.videojsLocaleCache[path]) + } else { + p = fetch(path + '/player.json') + .then(res => res.json()) + .then(json => { + PeertubePlayerManager.videojsLocaleCache[path] = json + return json + }) + .catch(err => { + console.error('Cannot get player translations', err) + return undefined + }) + } + + const completeLocale = getCompleteLocale(locale) + return p.then(json => videojs.addLanguage(getShortLocale(completeLocale), json)) + } + + private static getVideojsOptions (mode: PlayerMode, options: PeertubePlayerManagerOptions) { + const commonOptions = options.common + const webtorrentOptions = options.webtorrent + const p2pMediaLoaderOptions = options.p2pMediaLoader + + const plugins: VideoJSPluginOptions = { + peertube: { + autoplay: commonOptions.autoplay, // Use peertube plugin autoplay because we get the file by webtorrent + videoViewUrl: commonOptions.videoViewUrl, + videoDuration: commonOptions.videoDuration, + startTime: commonOptions.startTime, + userWatching: commonOptions.userWatching, + subtitle: commonOptions.subtitle, + videoCaptions: commonOptions.videoCaptions + } + } + + if (p2pMediaLoaderOptions) { + const p2pMediaLoader: P2PMediaLoaderPluginOptions = { + type: 'application/x-mpegURL', + src: p2pMediaLoaderOptions.playlistUrl + } + + const config = { + segments: { + swarmId: 'swarm' // TODO: choose swarm id + } + } + const streamrootHls = { + html5: { + hlsjsConfig: { + liveSyncDurationCount: 7, + loader: new Engine(config).createLoaderClass() + } + } + } + + Object.assign(plugins, { p2pMediaLoader, streamrootHls }) + } + + if (webtorrentOptions) { + const webtorrent = { + autoplay: commonOptions.autoplay, + videoDuration: commonOptions.videoDuration, + playerElement: commonOptions.playerElement, + videoFiles: webtorrentOptions.videoFiles + } + Object.assign(plugins, { webtorrent }) + } + + const videojsOptions = { + // We don't use text track settings for now + textTrackSettings: false, + controls: commonOptions.controls !== undefined ? commonOptions.controls : true, + loop: commonOptions.loop !== undefined ? commonOptions.loop : false, + + muted: commonOptions.muted !== undefined + ? commonOptions.muted + : undefined, // Undefined so the player knows it has to check the local storage + + poster: commonOptions.poster, + autoplay: false, + inactivityTimeout: commonOptions.inactivityTimeout, + playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ], + plugins, + controlBar: { + children: this.getControlBarChildren(mode, { + captions: commonOptions.captions, + peertubeLink: commonOptions.peertubeLink, + theaterMode: commonOptions.theaterMode + }) + } + } + + if (commonOptions.enableHotkeys === true) { + Object.assign(videojsOptions.plugins, { + hotkeys: { + enableVolumeScroll: false, + enableModifiersForNumbers: false, + + fullscreenKey: function (event: KeyboardEvent) { + // fullscreen with the f key or Ctrl+Enter + return event.key === 'f' || (event.ctrlKey && event.key === 'Enter') + }, + + seekStep: function (event: KeyboardEvent) { + // mimic VLC seek behavior, and default to 5 (original value is 5). + if (event.ctrlKey && event.altKey) { + return 5 * 60 + } else if (event.ctrlKey) { + return 60 + } else if (event.altKey) { + return 10 + } else { + return 5 + } + }, + + customKeys: { + increasePlaybackRateKey: { + key: function (event: KeyboardEvent) { + return event.key === '>' + }, + handler: function (player: videojs.Player) { + player.playbackRate((player.playbackRate() + 0.1).toFixed(2)) + } + }, + decreasePlaybackRateKey: { + key: function (event: KeyboardEvent) { + return event.key === '<' + }, + handler: function (player: videojs.Player) { + player.playbackRate((player.playbackRate() - 0.1).toFixed(2)) + } + }, + frameByFrame: { + key: function (event: KeyboardEvent) { + return event.key === '.' + }, + handler: function (player: videojs.Player) { + player.pause() + // Calculate movement distance (assuming 30 fps) + const dist = 1 / 30 + player.currentTime(player.currentTime() + dist) + } + } + } + } + }) + } + + if (commonOptions.language && !isDefaultLocale(commonOptions.language)) { + Object.assign(videojsOptions, { language: commonOptions.language }) + } + + return videojsOptions + } + + private static getControlBarChildren (mode: PlayerMode, options: { + peertubeLink: boolean + theaterMode: boolean, + captions: boolean + }) { + const settingEntries = [] + const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar' + + // Keep an order + settingEntries.push('playbackRateMenuButton') + if (options.captions === true) settingEntries.push('captionsButton') + settingEntries.push('resolutionMenuButton') + + const children = { + 'playToggle': {}, + 'currentTimeDisplay': {}, + 'timeDivider': {}, + 'durationDisplay': {}, + 'liveDisplay': {}, + + 'flexibleWidthSpacer': {}, + 'progressControl': { + children: { + 'seekBar': { + children: { + [loadProgressBar]: {}, + 'mouseTimeDisplay': {}, + 'playProgressBar': {} + } + } + } + }, + + 'p2PInfoButton': {}, + + 'muteToggle': {}, + 'volumeControl': {}, + + 'settingsButton': { + setup: { + maxHeightOffset: 40 + }, + entries: settingEntries + } + } + + if (options.peertubeLink === true) { + Object.assign(children, { + 'peerTubeLinkButton': {} + }) + } + + if (options.theaterMode === true) { + Object.assign(children, { + 'theaterButton': {} + }) + } + + Object.assign(children, { + 'fullscreenToggle': {} + }) + + return children + } + + private static addContextMenu (mode: PlayerMode, player: any, videoEmbedUrl: string) { + const content = [ + { + label: player.localize('Copy the video URL'), + listener: function () { + copyToClipboard(buildVideoLink()) + } + }, + { + label: player.localize('Copy the video URL at the current time'), + listener: function () { + const player = this as videojs.Player + copyToClipboard(buildVideoLink(player.currentTime())) + } + }, + { + label: player.localize('Copy embed code'), + listener: () => { + copyToClipboard(buildVideoEmbed(videoEmbedUrl)) + } + } + ] + + if (mode === 'webtorrent') { + content.push({ + label: player.localize('Copy magnet URI'), + listener: function () { + const player = this as videojs.Player + copyToClipboard(player.webtorrent().getCurrentVideoFile().magnetUri) + } + }) + } + + player.contextmenuUI({ content }) + } + + private static getLocalePath (serverUrl: string, locale: string) { + const completeLocale = getCompleteLocale(locale) + + if (!is18nLocale(completeLocale) || isDefaultLocale(completeLocale)) return undefined + + return serverUrl + '/client/locales/' + completeLocale + } +} + +// ############################################################################ + +export { + videojs +} diff --git a/client/src/assets/player/peertube-player.ts b/client/src/assets/player/peertube-player.ts deleted file mode 100644 index 2de6d7fef..000000000 --- a/client/src/assets/player/peertube-player.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { VideoFile } from '../../../../shared/models/videos' - -import 'videojs-hotkeys' -import 'videojs-dock' -import 'videojs-contextmenu-ui' -import './peertube-link-button' -import './resolution-menu-button' -import './settings-menu-button' -import './webtorrent-info-button' -import './peertube-videojs-plugin' -import './peertube-load-progress-bar' -import './theater-button' -import { UserWatching, VideoJSCaption, videojsUntyped } from './peertube-videojs-typings' -import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils' -import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n' - -// FIXME: something weird with our path definition in tsconfig and typings -// @ts-ignore -import { Player } from 'video.js' - -// Change 'Playback Rate' to 'Speed' (smaller for our settings menu) -videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' -// Change Captions to Subtitles/CC -videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC' -// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) -videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' ' - -function getVideojsOptions (options: { - autoplay: boolean - playerElement: HTMLVideoElement - videoViewUrl: string - videoDuration: number - videoFiles: VideoFile[] - enableHotkeys: boolean - inactivityTimeout: number - peertubeLink: boolean - poster: string - startTime: number | string - theaterMode: boolean - videoCaptions: VideoJSCaption[] - - language?: string - controls?: boolean - muted?: boolean - loop?: boolean - subtitle?: string - - userWatching?: UserWatching -}) { - const videojsOptions = { - // We don't use text track settings for now - textTrackSettings: false, - controls: options.controls !== undefined ? options.controls : true, - loop: options.loop !== undefined ? options.loop : false, - - muted: options.muted !== undefined ? options.muted : undefined, // Undefined so the player knows it has to check the local storage - - poster: options.poster, - autoplay: false, - inactivityTimeout: options.inactivityTimeout, - playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ], - plugins: { - peertube: { - autoplay: options.autoplay, // Use peertube plugin autoplay because we get the file by webtorrent - videoCaptions: options.videoCaptions, - videoFiles: options.videoFiles, - playerElement: options.playerElement, - videoViewUrl: options.videoViewUrl, - videoDuration: options.videoDuration, - startTime: options.startTime, - userWatching: options.userWatching, - subtitle: options.subtitle - } - }, - controlBar: { - children: getControlBarChildren(options) - } - } - - if (options.enableHotkeys === true) { - Object.assign(videojsOptions.plugins, { - hotkeys: { - enableVolumeScroll: false, - enableModifiersForNumbers: false, - - fullscreenKey: function (event: KeyboardEvent) { - // fullscreen with the f key or Ctrl+Enter - return event.key === 'f' || (event.ctrlKey && event.key === 'Enter') - }, - - seekStep: function (event: KeyboardEvent) { - // mimic VLC seek behavior, and default to 5 (original value is 5). - if (event.ctrlKey && event.altKey) { - return 5 * 60 - } else if (event.ctrlKey) { - return 60 - } else if (event.altKey) { - return 10 - } else { - return 5 - } - }, - - customKeys: { - increasePlaybackRateKey: { - key: function (event: KeyboardEvent) { - return event.key === '>' - }, - handler: function (player: Player) { - player.playbackRate((player.playbackRate() + 0.1).toFixed(2)) - } - }, - decreasePlaybackRateKey: { - key: function (event: KeyboardEvent) { - return event.key === '<' - }, - handler: function (player: Player) { - player.playbackRate((player.playbackRate() - 0.1).toFixed(2)) - } - }, - frameByFrame: { - key: function (event: KeyboardEvent) { - return event.key === '.' - }, - handler: function (player: Player) { - player.pause() - // Calculate movement distance (assuming 30 fps) - const dist = 1 / 30 - player.currentTime(player.currentTime() + dist) - } - } - } - } - }) - } - - if (options.language && !isDefaultLocale(options.language)) { - Object.assign(videojsOptions, { language: options.language }) - } - - return videojsOptions -} - -function getControlBarChildren (options: { - peertubeLink: boolean - theaterMode: boolean, - videoCaptions: VideoJSCaption[] -}) { - const settingEntries = [] - - // Keep an order - settingEntries.push('playbackRateMenuButton') - if (options.videoCaptions.length !== 0) settingEntries.push('captionsButton') - settingEntries.push('resolutionMenuButton') - - const children = { - 'playToggle': {}, - 'currentTimeDisplay': {}, - 'timeDivider': {}, - 'durationDisplay': {}, - 'liveDisplay': {}, - - 'flexibleWidthSpacer': {}, - 'progressControl': { - children: { - 'seekBar': { - children: { - 'peerTubeLoadProgressBar': {}, - 'mouseTimeDisplay': {}, - 'playProgressBar': {} - } - } - } - }, - - 'webTorrentButton': {}, - - 'muteToggle': {}, - 'volumeControl': {}, - - 'settingsButton': { - setup: { - maxHeightOffset: 40 - }, - entries: settingEntries - } - } - - if (options.peertubeLink === true) { - Object.assign(children, { - 'peerTubeLinkButton': {} - }) - } - - if (options.theaterMode === true) { - Object.assign(children, { - 'theaterButton': {} - }) - } - - Object.assign(children, { - 'fullscreenToggle': {} - }) - - return children -} - -function addContextMenu (player: any, videoEmbedUrl: string) { - player.contextmenuUI({ - content: [ - { - label: player.localize('Copy the video URL'), - listener: function () { - copyToClipboard(buildVideoLink()) - } - }, - { - label: player.localize('Copy the video URL at the current time'), - listener: function () { - const player = this as Player - copyToClipboard(buildVideoLink(player.currentTime())) - } - }, - { - label: player.localize('Copy embed code'), - listener: () => { - copyToClipboard(buildVideoEmbed(videoEmbedUrl)) - } - }, - { - label: player.localize('Copy magnet URI'), - listener: function () { - const player = this as Player - copyToClipboard(player.peertube().getCurrentVideoFile().magnetUri) - } - } - ] - }) -} - -function loadLocaleInVideoJS (serverUrl: string, videojs: any, locale: string) { - const path = getLocalePath(serverUrl, locale) - // It is the default locale, nothing to translate - if (!path) return Promise.resolve(undefined) - - let p: Promise - - if (loadLocaleInVideoJS.cache[path]) { - p = Promise.resolve(loadLocaleInVideoJS.cache[path]) - } else { - p = fetch(path + '/player.json') - .then(res => res.json()) - .then(json => { - loadLocaleInVideoJS.cache[path] = json - return json - }) - .catch(err => { - console.error('Cannot get player translations', err) - return undefined - }) - } - - const completeLocale = getCompleteLocale(locale) - return p.then(json => videojs.addLanguage(getShortLocale(completeLocale), json)) -} -namespace loadLocaleInVideoJS { - export const cache: { [ path: string ]: any } = {} -} - -function getServerTranslations (serverUrl: string, locale: string) { - const path = getLocalePath(serverUrl, locale) - // It is the default locale, nothing to translate - if (!path) return Promise.resolve(undefined) - - return fetch(path + '/server.json') - .then(res => res.json()) - .catch(err => { - console.error('Cannot get server translations', err) - return undefined - }) -} - -// ############################################################################ - -export { - getServerTranslations, - loadLocaleInVideoJS, - getVideojsOptions, - addContextMenu -} - -// ############################################################################ - -function getLocalePath (serverUrl: string, locale: string) { - const completeLocale = getCompleteLocale(locale) - - if (!is18nLocale(completeLocale) || isDefaultLocale(completeLocale)) return undefined - - return serverUrl + '/client/locales/' + completeLocale -} diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts new file mode 100644 index 000000000..0bd607697 --- /dev/null +++ b/client/src/assets/player/peertube-plugin.ts @@ -0,0 +1,219 @@ +// FIXME: something weird with our path definition in tsconfig and typings +// @ts-ignore +import * as videojs from 'video.js' +import './videojs-components/settings-menu-button' +import { PeerTubePluginOptions, UserWatching, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' +import { isMobile, timeToInt } from './utils' +import { + getStoredLastSubtitle, + getStoredMute, + getStoredVolume, + saveLastSubtitle, + saveMuteInStore, + saveVolumeInStore +} from './peertube-player-local-storage' + +const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') +class PeerTubePlugin extends Plugin { + private readonly autoplay: boolean = false + private readonly startTime: number = 0 + private readonly videoViewUrl: string + private readonly videoDuration: number + private readonly CONSTANTS = { + USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video + } + + private player: any + private videoCaptions: VideoJSCaption[] + private defaultSubtitle: string + + private videoViewInterval: any + private userWatchingVideoInterval: any + private qualityObservationTimer: any + + constructor (player: videojs.Player, options: PeerTubePluginOptions) { + super(player, options) + + this.startTime = timeToInt(options.startTime) + this.videoViewUrl = options.videoViewUrl + this.videoDuration = options.videoDuration + this.videoCaptions = options.videoCaptions + + if (this.autoplay === true) this.player.addClass('vjs-has-autoplay') + + this.player.ready(() => { + const playerOptions = this.player.options_ + + const volume = getStoredVolume() + if (volume !== undefined) this.player.volume(volume) + + const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() + if (muted !== undefined) this.player.muted(muted) + + this.defaultSubtitle = options.subtitle || getStoredLastSubtitle() + + this.player.on('volumechange', () => { + saveVolumeInStore(this.player.volume()) + saveMuteInStore(this.player.muted()) + }) + + this.player.textTracks().on('change', () => { + const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => { + return t.kind === 'captions' && t.mode === 'showing' + }) + + if (!showing) { + saveLastSubtitle('off') + return + } + + saveLastSubtitle(showing.language) + }) + + this.player.on('sourcechange', () => this.initCaptions()) + + this.player.duration(options.videoDuration) + + this.initializePlayer() + this.runViewAdd() + + if (options.userWatching) this.runUserWatchVideo(options.userWatching) + }) + } + + dispose () { + clearTimeout(this.qualityObservationTimer) + + clearInterval(this.videoViewInterval) + + if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval) + } + + private initializePlayer () { + if (isMobile()) this.player.addClass('vjs-is-mobile') + + this.initSmoothProgressBar() + + this.initCaptions() + + this.alterInactivity() + } + + private runViewAdd () { + this.clearVideoViewInterval() + + // After 30 seconds (or 3/4 of the video), add a view to the video + let minSecondsToView = 30 + + if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4 + + let secondsViewed = 0 + this.videoViewInterval = setInterval(() => { + if (this.player && !this.player.paused()) { + secondsViewed += 1 + + if (secondsViewed > minSecondsToView) { + this.clearVideoViewInterval() + + this.addViewToVideo().catch(err => console.error(err)) + } + } + }, 1000) + } + + private runUserWatchVideo (options: UserWatching) { + let lastCurrentTime = 0 + + this.userWatchingVideoInterval = setInterval(() => { + const currentTime = Math.floor(this.player.currentTime()) + + if (currentTime - lastCurrentTime >= 1) { + lastCurrentTime = currentTime + + this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader) + .catch(err => console.error('Cannot notify user is watching.', err)) + } + }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL) + } + + private clearVideoViewInterval () { + if (this.videoViewInterval !== undefined) { + clearInterval(this.videoViewInterval) + this.videoViewInterval = undefined + } + } + + private addViewToVideo () { + if (!this.videoViewUrl) return Promise.resolve(undefined) + + return fetch(this.videoViewUrl, { method: 'POST' }) + } + + private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) { + const body = new URLSearchParams() + body.append('currentTime', currentTime.toString()) + + const headers = new Headers({ 'Authorization': authorizationHeader }) + + return fetch(url, { method: 'PUT', body, headers }) + } + + private alterInactivity () { + let saveInactivityTimeout: number + + const disableInactivity = () => { + saveInactivityTimeout = this.player.options_.inactivityTimeout + this.player.options_.inactivityTimeout = 0 + } + const enableInactivity = () => { + this.player.options_.inactivityTimeout = saveInactivityTimeout + } + + const settingsDialog = this.player.children_.find((c: any) => c.name_ === 'SettingsDialog') + + this.player.controlBar.on('mouseenter', () => disableInactivity()) + settingsDialog.on('mouseenter', () => disableInactivity()) + this.player.controlBar.on('mouseleave', () => enableInactivity()) + settingsDialog.on('mouseleave', () => enableInactivity()) + } + + private initCaptions () { + for (const caption of this.videoCaptions) { + this.player.addRemoteTextTrack({ + kind: 'captions', + label: caption.label, + language: caption.language, + id: caption.language, + src: caption.src, + default: this.defaultSubtitle === caption.language + }, false) + } + + this.player.trigger('captionsChanged') + } + + // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 + private initSmoothProgressBar () { + const SeekBar = videojsUntyped.getComponent('SeekBar') + SeekBar.prototype.getPercent = function getPercent () { + // Allows for smooth scrubbing, when player can't keep up. + // const time = (this.player_.scrubbing()) ? + // this.player_.getCache().currentTime : + // this.player_.currentTime() + const time = this.player_.currentTime() + const percent = time / this.player_.duration() + return percent >= 1 ? 1 : percent + } + SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) { + let newTime = this.calculateDistance(event) * this.player_.duration() + if (newTime === this.player_.duration()) { + newTime = newTime - 0.1 + } + this.player_.currentTime(newTime) + this.update() + } + } +} + +videojs.registerPlugin('peertube', PeerTubePlugin) +export { PeerTubePlugin } diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts index 634c7fdc9..060ea4dce 100644 --- a/client/src/assets/player/peertube-videojs-typings.ts +++ b/client/src/assets/player/peertube-videojs-typings.ts @@ -3,11 +3,13 @@ import * as videojs from 'video.js' import { VideoFile } from '../../../../shared/models/videos/video.model' -import { PeerTubePlugin } from './peertube-videojs-plugin' +import { PeerTubePlugin } from './peertube-plugin' +import { WebTorrentPlugin } from './webtorrent-plugin' declare namespace videojs { interface Player { peertube (): PeerTubePlugin + webtorrent (): WebTorrentPlugin } } @@ -30,26 +32,73 @@ type UserWatching = { authorizationHeader: string } -type PeertubePluginOptions = { - videoFiles: VideoFile[] - playerElement: HTMLVideoElement +type PeerTubePluginOptions = { + autoplay: boolean videoViewUrl: string videoDuration: number startTime: number | string - autoplay: boolean, - videoCaptions: VideoJSCaption[] - subtitle?: string userWatching?: UserWatching + subtitle?: string + + videoCaptions: VideoJSCaption[] +} + +type WebtorrentPluginOptions = { + playerElement: HTMLVideoElement + + autoplay: boolean + videoDuration: number + + videoFiles: VideoFile[] +} + +type P2PMediaLoaderPluginOptions = { + type: string + src: string +} + +type VideoJSPluginOptions = { + peertube: PeerTubePluginOptions + + webtorrent?: WebtorrentPluginOptions + + p2pMediaLoader?: P2PMediaLoaderPluginOptions } // videojs typings don't have some method we need const videojsUntyped = videojs as any +type LoadedQualityData = { + qualitySwitchCallback: Function, + qualityData: { + video: { + id: number + label: string + selected: boolean + }[] + } +} + +type ResolutionUpdateData = { + auto: boolean, + resolutionId: number +} + +type AutoResolutionUpdateData = { + possible: boolean +} + export { + ResolutionUpdateData, + AutoResolutionUpdateData, VideoJSComponentInterface, - PeertubePluginOptions, videojsUntyped, VideoJSCaption, - UserWatching + UserWatching, + PeerTubePluginOptions, + WebtorrentPluginOptions, + P2PMediaLoaderPluginOptions, + VideoJSPluginOptions, + LoadedQualityData } diff --git a/client/src/assets/player/resolution-menu-item.ts b/client/src/assets/player/resolution-menu-item.ts deleted file mode 100644 index b54fd91ef..000000000 --- a/client/src/assets/player/resolution-menu-item.ts +++ /dev/null @@ -1,67 +0,0 @@ -// FIXME: something weird with our path definition in tsconfig and typings -// @ts-ignore -import { Player } from 'video.js' - -import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' - -const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') -class ResolutionMenuItem extends MenuItem { - - constructor (player: Player, options: any) { - const currentResolutionId = player.peertube().getCurrentResolutionId() - options.selectable = true - options.selected = options.id === currentResolutionId - - super(player, options) - - this.label = options.label - this.id = options.id - - player.peertube().on('videoFileUpdate', () => this.updateSelection()) - player.peertube().on('autoResolutionUpdate', () => this.updateSelection()) - } - - handleClick (event: any) { - if (this.id === -1 && this.player_.peertube().isAutoResolutionForbidden()) return - - super.handleClick(event) - - // Auto resolution - if (this.id === -1) { - this.player_.peertube().enableAutoResolution() - return - } - - this.player_.peertube().disableAutoResolution() - this.player_.peertube().updateResolution(this.id) - } - - updateSelection () { - // Check if auto resolution is forbidden or not - if (this.id === -1) { - if (this.player_.peertube().isAutoResolutionForbidden()) { - this.addClass('disabled') - } else { - this.removeClass('disabled') - } - } - - if (this.player_.peertube().isAutoResolutionOn()) { - this.selected(this.id === -1) - return - } - - this.selected(this.player_.peertube().getCurrentResolutionId() === this.id) - } - - getLabel () { - if (this.id === -1) { - return this.label + ' ' + this.player_.peertube().getCurrentResolutionLabel() + '' - } - - return this.label - } -} -MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) - -export { ResolutionMenuItem } diff --git a/client/src/assets/player/webtorrent-info-button.ts b/client/src/assets/player/videojs-components/p2p-info-button.ts similarity index 91% rename from client/src/assets/player/webtorrent-info-button.ts rename to client/src/assets/player/videojs-components/p2p-info-button.ts index c3c1af951..03a5d29f0 100644 --- a/client/src/assets/player/webtorrent-info-button.ts +++ b/client/src/assets/player/videojs-components/p2p-info-button.ts @@ -1,8 +1,8 @@ -import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' -import { bytes } from './utils' +import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' +import { bytes } from '../utils' const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') -class WebtorrentInfoButton extends Button { +class P2pInfoButton extends Button { createEl () { const div = videojsUntyped.dom.createEl('div', { @@ -65,7 +65,7 @@ class WebtorrentInfoButton extends Button { subDivHttp.appendChild(subDivHttpText) div.appendChild(subDivHttp) - this.player_.peertube().on('torrentInfo', (event: any, data: any) => { + this.player_.on('p2pInfo', (event: any, data: any) => { // We are in HTTP fallback if (!data) { subDivHttp.className = 'vjs-peertube-displayed' @@ -99,4 +99,4 @@ class WebtorrentInfoButton extends Button { return div } } -Button.registerComponent('WebTorrentButton', WebtorrentInfoButton) +Button.registerComponent('P2PInfoButton', P2pInfoButton) diff --git a/client/src/assets/player/peertube-link-button.ts b/client/src/assets/player/videojs-components/peertube-link-button.ts similarity index 87% rename from client/src/assets/player/peertube-link-button.ts rename to client/src/assets/player/videojs-components/peertube-link-button.ts index de9a49de9..fed8ea33e 100644 --- a/client/src/assets/player/peertube-link-button.ts +++ b/client/src/assets/player/videojs-components/peertube-link-button.ts @@ -1,5 +1,5 @@ -import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' -import { buildVideoLink } from './utils' +import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' +import { buildVideoLink } from '../utils' // FIXME: something weird with our path definition in tsconfig and typings // @ts-ignore import { Player } from 'video.js' diff --git a/client/src/assets/player/peertube-load-progress-bar.ts b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts similarity index 85% rename from client/src/assets/player/peertube-load-progress-bar.ts rename to client/src/assets/player/videojs-components/peertube-load-progress-bar.ts index af276d1b2..9a0e3b550 100644 --- a/client/src/assets/player/peertube-load-progress-bar.ts +++ b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts @@ -1,4 +1,4 @@ -import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' +import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' // FIXME: something weird with our path definition in tsconfig and typings // @ts-ignore import { Player } from 'video.js' @@ -27,7 +27,7 @@ class PeerTubeLoadProgressBar extends Component { } update () { - const torrent = this.player().peertube().getTorrent() + const torrent = this.player().webtorrent().getTorrent() if (!torrent) return this.el_.style.width = (torrent.progress * 100) + '%' diff --git a/client/src/assets/player/resolution-menu-button.ts b/client/src/assets/player/videojs-components/resolution-menu-button.ts similarity index 56% rename from client/src/assets/player/resolution-menu-button.ts rename to client/src/assets/player/videojs-components/resolution-menu-button.ts index a3c1108ca..2847de470 100644 --- a/client/src/assets/player/resolution-menu-button.ts +++ b/client/src/assets/player/videojs-components/resolution-menu-button.ts @@ -2,7 +2,7 @@ // @ts-ignore import { Player } from 'video.js' -import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' +import { LoadedQualityData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' import { ResolutionMenuItem } from './resolution-menu-item' const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') @@ -14,16 +14,18 @@ class ResolutionMenuButton extends MenuButton { super(player, options) this.player = player - player.peertube().on('videoFileUpdate', () => this.updateLabel()) - player.peertube().on('autoResolutionUpdate', () => this.updateLabel()) + player.on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data)) + + if (player.webtorrent) { + player.webtorrent().on('videoFileUpdate', () => setTimeout(() => this.trigger('updateLabel'), 0)) + } } createEl () { const el = super.createEl() this.labelEl_ = videojsUntyped.dom.createEl('div', { - className: 'vjs-resolution-value', - innerHTML: this.buildLabelHTML() + className: 'vjs-resolution-value' }) el.appendChild(this.labelEl_) @@ -36,51 +38,45 @@ class ResolutionMenuButton extends MenuButton { } createMenu () { - const menu = new Menu(this.player_) - for (const videoFile of this.player_.peertube().videoFiles) { - let label = videoFile.resolution.label - if (videoFile.fps && videoFile.fps >= 50) { - label += videoFile.fps - } + return new Menu(this.player_) + } + + buildCSSClass () { + return super.buildCSSClass() + ' vjs-resolution-button' + } - menu.addChild(new ResolutionMenuItem( + buildWrapperCSSClass () { + return 'vjs-resolution-control ' + super.buildWrapperCSSClass() + } + + private buildQualities (data: LoadedQualityData) { + // The automatic resolution item will need other labels + const labels: { [ id: number ]: string } = {} + + for (const d of data.qualityData.video) { + this.menu.addChild(new ResolutionMenuItem( this.player_, { - id: videoFile.resolution.id, - label, - src: videoFile.magnetUri + id: d.id, + label: d.label, + selected: d.selected, + callback: data.qualitySwitchCallback }) ) + + labels[d.id] = d.label } - menu.addChild(new ResolutionMenuItem( + this.menu.addChild(new ResolutionMenuItem( this.player_, { id: -1, label: this.player_.localize('Auto'), - src: null + labels, + callback: data.qualitySwitchCallback, + selected: true // By default, in auto mode } )) - - return menu - } - - updateLabel () { - if (!this.labelEl_) return - - this.labelEl_.innerHTML = this.buildLabelHTML() - } - - buildCSSClass () { - return super.buildCSSClass() + ' vjs-resolution-button' - } - - buildWrapperCSSClass () { - return 'vjs-resolution-control ' + super.buildWrapperCSSClass() - } - - private buildLabelHTML () { - return this.player_.peertube().getCurrentResolutionLabel() } } ResolutionMenuButton.prototype.controlText_ = 'Quality' diff --git a/client/src/assets/player/videojs-components/resolution-menu-item.ts b/client/src/assets/player/videojs-components/resolution-menu-item.ts new file mode 100644 index 000000000..cc1c79739 --- /dev/null +++ b/client/src/assets/player/videojs-components/resolution-menu-item.ts @@ -0,0 +1,87 @@ +// FIXME: something weird with our path definition in tsconfig and typings +// @ts-ignore +import { Player } from 'video.js' + +import { AutoResolutionUpdateData, ResolutionUpdateData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' + +const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') +class ResolutionMenuItem extends MenuItem { + private readonly id: number + private readonly label: string + // Only used for the automatic item + private readonly labels: { [id: number]: string } + private readonly callback: Function + + private autoResolutionPossible: boolean + private currentResolutionLabel: string + + constructor (player: Player, options: any) { + options.selectable = true + + super(player, options) + + this.autoResolutionPossible = true + this.currentResolutionLabel = '' + + this.label = options.label + this.labels = options.labels + this.id = options.id + this.callback = options.callback + + if (player.webtorrent) { + player.webtorrent().on('videoFileUpdate', (_: any, data: ResolutionUpdateData) => this.updateSelection(data)) + + // We only want to disable the "Auto" item + if (this.id === -1) { + player.webtorrent().on('autoResolutionUpdate', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data)) + } + } + + // TODO: update on HLS change + } + + handleClick (event: any) { + // Auto button disabled? + if (this.autoResolutionPossible === false && this.id === -1) return + + super.handleClick(event) + + this.callback(this.id) + } + + updateSelection (data: ResolutionUpdateData) { + if (this.id === -1) { + this.currentResolutionLabel = this.labels[data.resolutionId] + } + + // Automatic resolution only + if (data.auto === true) { + this.selected(this.id === -1) + return + } + + this.selected(this.id === data.resolutionId) + } + + updateAutoResolution (data: AutoResolutionUpdateData) { + // Check if the auto resolution is enabled or not + if (data.possible === false) { + this.addClass('disabled') + } else { + this.removeClass('disabled') + } + + this.autoResolutionPossible = data.possible + } + + getLabel () { + if (this.id === -1) { + return this.label + ' ' + this.currentResolutionLabel + '' + } + + return this.label + } +} +MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) + +export { ResolutionMenuItem } diff --git a/client/src/assets/player/settings-menu-button.ts b/client/src/assets/player/videojs-components/settings-menu-button.ts similarity index 98% rename from client/src/assets/player/settings-menu-button.ts rename to client/src/assets/player/videojs-components/settings-menu-button.ts index a7aefdcc3..14cb8ba43 100644 --- a/client/src/assets/player/settings-menu-button.ts +++ b/client/src/assets/player/videojs-components/settings-menu-button.ts @@ -6,8 +6,8 @@ import * as videojs from 'video.js' import { SettingsMenuItem } from './settings-menu-item' -import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' -import { toTitleCase } from './utils' +import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' +import { toTitleCase } from '../utils' const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') diff --git a/client/src/assets/player/settings-menu-item.ts b/client/src/assets/player/videojs-components/settings-menu-item.ts similarity index 97% rename from client/src/assets/player/settings-menu-item.ts rename to client/src/assets/player/videojs-components/settings-menu-item.ts index 2a3460ae5..b9a430290 100644 --- a/client/src/assets/player/settings-menu-item.ts +++ b/client/src/assets/player/videojs-components/settings-menu-item.ts @@ -5,8 +5,8 @@ // @ts-ignore import * as videojs from 'video.js' -import { toTitleCase } from './utils' -import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' +import { toTitleCase } from '../utils' +import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') @@ -220,12 +220,9 @@ class SettingsMenuItem extends MenuItem { } build () { - const saveUpdateLabel = this.subMenu.updateLabel - this.subMenu.updateLabel = () => { + this.subMenu.on('updateLabel', () => { this.update() - - saveUpdateLabel.call(this.subMenu) - } + }) this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_) this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) diff --git a/client/src/assets/player/theater-button.ts b/client/src/assets/player/videojs-components/theater-button.ts similarity index 87% rename from client/src/assets/player/theater-button.ts rename to client/src/assets/player/videojs-components/theater-button.ts index 4f8fede3d..1e11a9546 100644 --- a/client/src/assets/player/theater-button.ts +++ b/client/src/assets/player/videojs-components/theater-button.ts @@ -2,8 +2,8 @@ // @ts-ignore import * as videojs from 'video.js' -import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' -import { saveTheaterInStore, getStoredTheater } from './peertube-player-local-storage' +import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' +import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage' const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') class TheaterButton extends Button { diff --git a/client/src/assets/player/peertube-videojs-plugin.ts b/client/src/assets/player/webtorrent-plugin.ts similarity index 73% rename from client/src/assets/player/peertube-videojs-plugin.ts rename to client/src/assets/player/webtorrent-plugin.ts index e9fb90c61..c3d990aed 100644 --- a/client/src/assets/player/peertube-videojs-plugin.ts +++ b/client/src/assets/player/webtorrent-plugin.ts @@ -4,21 +4,16 @@ import * as videojs from 'video.js' import * as WebTorrent from 'webtorrent' import { VideoFile } from '../../../../shared/models/videos/video.model' -import { renderVideo } from './video-renderer' -import './settings-menu-button' -import { PeertubePluginOptions, UserWatching, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' -import { isMobile, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from './utils' -import { PeertubeChunkStore } from './peertube-chunk-store' +import { renderVideo } from './webtorrent/video-renderer' +import { LoadedQualityData, VideoJSComponentInterface, WebtorrentPluginOptions } from './peertube-videojs-typings' +import { videoFileMaxByResolution, videoFileMinByResolution } from './utils' +import { PeertubeChunkStore } from './webtorrent/peertube-chunk-store' import { getAverageBandwidthInStore, - getStoredLastSubtitle, getStoredMute, getStoredVolume, getStoredWebTorrentEnabled, - saveAverageBandwidth, - saveLastSubtitle, - saveMuteInStore, - saveVolumeInStore + saveAverageBandwidth } from './peertube-player-local-storage' const CacheChunkStore = require('cache-chunk-store') @@ -30,14 +25,13 @@ type PlayOptions = { } const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') -class PeerTubePlugin extends Plugin { +class WebTorrentPlugin extends Plugin { private readonly playerElement: HTMLVideoElement private readonly autoplay: boolean = false private readonly startTime: number = 0 private readonly savePlayerSrcFunction: Function private readonly videoFiles: VideoFile[] - private readonly videoViewUrl: string private readonly videoDuration: number private readonly CONSTANTS = { INFO_SCHEDULER: 1000, // Don't change this @@ -45,8 +39,7 @@ class PeerTubePlugin extends Plugin { AUTO_QUALITY_THRESHOLD_PERCENT: 30, // Bandwidth should be 30% more important than a resolution bitrate to change to it AUTO_QUALITY_OBSERVATION_TIME: 10000, // Wait 10 seconds after having change the resolution before another check AUTO_QUALITY_HIGHER_RESOLUTION_DELAY: 5000, // Buffering higher resolution during 5 seconds - BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5, // Last 5 seconds to build average bandwidth - USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video + BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5 // Last 5 seconds to build average bandwidth } private readonly webtorrent = new WebTorrent({ @@ -68,46 +61,37 @@ class PeerTubePlugin extends Plugin { private player: any private currentVideoFile: VideoFile private torrent: WebTorrent.Torrent - private videoCaptions: VideoJSCaption[] - private defaultSubtitle: string private renderer: any private fakeRenderer: any private destroyingFakeRenderer = false private autoResolution = true - private forbidAutoResolution = false + private autoResolutionPossible = true private isAutoResolutionObservation = false private playerRefusedP2P = false - private videoViewInterval: any private torrentInfoInterval: any private autoQualityInterval: any - private userWatchingVideoInterval: any private addTorrentDelay: any private qualityObservationTimer: any private runAutoQualitySchedulerTimer: any private downloadSpeeds: number[] = [] - constructor (player: videojs.Player, options: PeertubePluginOptions) { + constructor (player: videojs.Player, options: WebtorrentPluginOptions) { super(player, options) // Disable auto play on iOS this.autoplay = options.autoplay && this.isIOS() === false this.playerRefusedP2P = !getStoredWebTorrentEnabled() - this.startTime = timeToInt(options.startTime) this.videoFiles = options.videoFiles - this.videoViewUrl = options.videoViewUrl this.videoDuration = options.videoDuration - this.videoCaptions = options.videoCaptions this.savePlayerSrcFunction = this.player.src this.playerElement = options.playerElement - if (this.autoplay === true) this.player.addClass('vjs-has-autoplay') - this.player.ready(() => { const playerOptions = this.player.options_ @@ -117,33 +101,10 @@ class PeerTubePlugin extends Plugin { const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() if (muted !== undefined) this.player.muted(muted) - this.defaultSubtitle = options.subtitle || getStoredLastSubtitle() - - this.player.on('volumechange', () => { - saveVolumeInStore(this.player.volume()) - saveMuteInStore(this.player.muted()) - }) - - this.player.textTracks().on('change', () => { - const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => { - return t.kind === 'captions' && t.mode === 'showing' - }) - - if (!showing) { - saveLastSubtitle('off') - return - } - - saveLastSubtitle(showing.language) - }) - this.player.duration(options.videoDuration) this.initializePlayer() this.runTorrentInfoScheduler() - this.runViewAdd() - - if (options.userWatching) this.runUserWatchVideo(options.userWatching) this.player.one('play', () => { // Don't run immediately scheduler, wait some seconds the TCP connections are made @@ -157,12 +118,9 @@ class PeerTubePlugin extends Plugin { clearTimeout(this.qualityObservationTimer) clearTimeout(this.runAutoQualitySchedulerTimer) - clearInterval(this.videoViewInterval) clearInterval(this.torrentInfoInterval) clearInterval(this.autoQualityInterval) - if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval) - // Don't need to destroy renderer, video player will be destroyed this.flushVideoFile(this.currentVideoFile, false) @@ -173,13 +131,6 @@ class PeerTubePlugin extends Plugin { return this.currentVideoFile ? this.currentVideoFile.resolution.id : -1 } - getCurrentResolutionLabel () { - if (!this.currentVideoFile) return '' - - const fps = this.currentVideoFile.fps >= 50 ? this.currentVideoFile.fps : '' - return this.currentVideoFile.resolution.label + fps - } - updateVideoFile ( videoFile?: VideoFile, options: { @@ -228,7 +179,8 @@ class PeerTubePlugin extends Plugin { return done() }) - this.trigger('videoFileUpdate') + this.changeQuality() + this.trigger('videoFileUpdate', { auto: this.autoResolution, resolutionId: this.currentVideoFile.resolution.id }) } updateResolution (resolutionId: number, delay = 0) { @@ -262,28 +214,17 @@ class PeerTubePlugin extends Plugin { } } - isAutoResolutionOn () { - return this.autoResolution - } - enableAutoResolution () { this.autoResolution = true - this.trigger('autoResolutionUpdate') + this.trigger('videoFileUpdate', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() }) } disableAutoResolution (forbid = false) { - if (forbid === true) this.forbidAutoResolution = true + if (forbid === true) this.autoResolutionPossible = false this.autoResolution = false - this.trigger('autoResolutionUpdate') - } - - isAutoResolutionForbidden () { - return this.forbidAutoResolution === true - } - - getCurrentVideoFile () { - return this.currentVideoFile + this.trigger('autoResolutionUpdate', { possible: this.autoResolutionPossible }) + this.trigger('videoFileUpdate', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() }) } getTorrent () { @@ -462,13 +403,7 @@ class PeerTubePlugin extends Plugin { } private initializePlayer () { - if (isMobile()) this.player.addClass('vjs-is-mobile') - - this.initSmoothProgressBar() - - this.initCaptions() - - this.alterInactivity() + this.buildQualities() if (this.autoplay === true) { this.player.posterImage.hide() @@ -491,7 +426,7 @@ class PeerTubePlugin extends Plugin { // Not initialized or in HTTP fallback if (this.torrent === undefined || this.torrent === null) return - if (this.isAutoResolutionOn() === false) return + if (this.autoResolution === false) return if (this.isAutoResolutionObservation === true) return const file = this.getAppropriateFile() @@ -531,12 +466,12 @@ class PeerTubePlugin extends Plugin { if (this.torrent === undefined) return // Http fallback - if (this.torrent === null) return this.trigger('torrentInfo', false) + if (this.torrent === null) return this.player.trigger('p2pInfo', false) // this.webtorrent.downloadSpeed because we need to take into account the potential old torrent too if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed) - return this.trigger('torrentInfo', { + return this.player.trigger('p2pInfo', { downloadSpeed: this.torrent.downloadSpeed, numPeers: this.torrent.numPeers, uploadSpeed: this.torrent.uploadSpeed, @@ -546,65 +481,6 @@ class PeerTubePlugin extends Plugin { }, this.CONSTANTS.INFO_SCHEDULER) } - private runViewAdd () { - this.clearVideoViewInterval() - - // After 30 seconds (or 3/4 of the video), add a view to the video - let minSecondsToView = 30 - - if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4 - - let secondsViewed = 0 - this.videoViewInterval = setInterval(() => { - if (this.player && !this.player.paused()) { - secondsViewed += 1 - - if (secondsViewed > minSecondsToView) { - this.clearVideoViewInterval() - - this.addViewToVideo().catch(err => console.error(err)) - } - } - }, 1000) - } - - private runUserWatchVideo (options: UserWatching) { - let lastCurrentTime = 0 - - this.userWatchingVideoInterval = setInterval(() => { - const currentTime = Math.floor(this.player.currentTime()) - - if (currentTime - lastCurrentTime >= 1) { - lastCurrentTime = currentTime - - this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader) - .catch(err => console.error('Cannot notify user is watching.', err)) - } - }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL) - } - - private clearVideoViewInterval () { - if (this.videoViewInterval !== undefined) { - clearInterval(this.videoViewInterval) - this.videoViewInterval = undefined - } - } - - private addViewToVideo () { - if (!this.videoViewUrl) return Promise.resolve(undefined) - - return fetch(this.videoViewUrl, { method: 'POST' }) - } - - private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) { - const body = new URLSearchParams() - body.append('currentTime', currentTime.toString()) - - const headers = new Headers({ 'Authorization': authorizationHeader }) - - return fetch(url, { method: 'PUT', body, headers }) - } - private fallbackToHttp (options: PlayOptions, done?: Function) { const paused = this.player.paused() @@ -620,8 +496,10 @@ class PeerTubePlugin extends Plugin { this.player.src = this.savePlayerSrcFunction this.player.src(httpUrl) + this.changeQuality() + // We changed the source, so reinit captions - this.initCaptions() + this.player.trigger('sourcechange') return this.tryToPlay(err => { if (err && done) return done(err) @@ -649,25 +527,6 @@ class PeerTubePlugin extends Plugin { return !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform) } - private alterInactivity () { - let saveInactivityTimeout: number - - const disableInactivity = () => { - saveInactivityTimeout = this.player.options_.inactivityTimeout - this.player.options_.inactivityTimeout = 0 - } - const enableInactivity = () => { - this.player.options_.inactivityTimeout = saveInactivityTimeout - } - - const settingsDialog = this.player.children_.find((c: any) => c.name_ === 'SettingsDialog') - - this.player.controlBar.on('mouseenter', () => disableInactivity()) - settingsDialog.on('mouseenter', () => disableInactivity()) - this.player.controlBar.on('mouseleave', () => enableInactivity()) - settingsDialog.on('mouseleave', () => enableInactivity()) - } - private pickAverageVideoFile () { if (this.videoFiles.length === 1) return this.videoFiles[0] @@ -712,43 +571,70 @@ class PeerTubePlugin extends Plugin { } } - private initCaptions () { - for (const caption of this.videoCaptions) { - this.player.addRemoteTextTrack({ - kind: 'captions', - label: caption.label, - language: caption.language, - id: caption.language, - src: caption.src, - default: this.defaultSubtitle === caption.language - }, false) + private buildQualities () { + const qualityLevelsPayload = [] + + for (const file of this.videoFiles) { + const representation = { + id: file.resolution.id, + label: this.buildQualityLabel(file), + height: file.resolution.id, + _enabled: true + } + + this.player.qualityLevels().addQualityLevel(representation) + + qualityLevelsPayload.push({ + id: representation.id, + label: representation.label, + selected: false + }) } - this.player.trigger('captionsChanged') + const payload: LoadedQualityData = { + qualitySwitchCallback: (d: any) => this.qualitySwitchCallback(d), + qualityData: { + video: qualityLevelsPayload + } + } + this.player.trigger('loadedqualitydata', payload) } - // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 - private initSmoothProgressBar () { - const SeekBar = videojsUntyped.getComponent('SeekBar') - SeekBar.prototype.getPercent = function getPercent () { - // Allows for smooth scrubbing, when player can't keep up. - // const time = (this.player_.scrubbing()) ? - // this.player_.getCache().currentTime : - // this.player_.currentTime() - const time = this.player_.currentTime() - const percent = time / this.player_.duration() - return percent >= 1 ? 1 : percent + private buildQualityLabel (file: VideoFile) { + let label = file.resolution.label + + if (file.fps && file.fps >= 50) { + label += file.fps } - SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) { - let newTime = this.calculateDistance(event) * this.player_.duration() - if (newTime === this.player_.duration()) { - newTime = newTime - 0.1 - } - this.player_.currentTime(newTime) - this.update() + + return label + } + + private qualitySwitchCallback (id: number) { + if (id === -1) { + if (this.autoResolutionPossible === true) this.enableAutoResolution() + return + } + + this.disableAutoResolution() + this.updateResolution(id) + } + + private changeQuality () { + const resolutionId = this.currentVideoFile.resolution.id + const qualityLevels = this.player.qualityLevels() + + if (resolutionId === -1) { + qualityLevels.selectedIndex = -1 + return + } + + for (let i = 0; i < qualityLevels; i++) { + const q = this.player.qualityLevels[i] + if (q.height === resolutionId) qualityLevels.selectedIndex = i } } } -videojs.registerPlugin('peertube', PeerTubePlugin) -export { PeerTubePlugin } +videojs.registerPlugin('webtorrent', WebTorrentPlugin) +export { WebTorrentPlugin } diff --git a/client/src/assets/player/peertube-chunk-store.ts b/client/src/assets/player/webtorrent/peertube-chunk-store.ts similarity index 100% rename from client/src/assets/player/peertube-chunk-store.ts rename to client/src/assets/player/webtorrent/peertube-chunk-store.ts diff --git a/client/src/assets/player/video-renderer.ts b/client/src/assets/player/webtorrent/video-renderer.ts similarity index 100% rename from client/src/assets/player/video-renderer.ts rename to client/src/assets/player/webtorrent/video-renderer.ts diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index 54b8fb543..b1261c4a2 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts @@ -17,17 +17,13 @@ import 'core-js/es6/set' // For google bot that uses Chrome 41 and does not understand fetch import 'whatwg-fetch' -// FIXME: something weird with our path definition in tsconfig and typings -// @ts-ignore -import * as vjs from 'video.js' - import * as Channel from 'jschannel' import { peertubeTranslate, ResultList, VideoDetails } from '../../../../shared' -import { addContextMenu, getServerTranslations, getVideojsOptions, loadLocaleInVideoJS } from '../../assets/player/peertube-player' import { PeerTubeResolution } from '../player/definitions' import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' +import { PeertubePlayerManager, PeertubePlayerManagerOptions } from '../../assets/player/peertube-player-manager' /** * Embed API exposes control of the embed player to the outside world via @@ -73,16 +69,16 @@ class PeerTubeEmbedApi { } private setResolution (resolutionId: number) { - if (resolutionId === -1 && this.embed.player.peertube().isAutoResolutionForbidden()) return + if (resolutionId === -1 && this.embed.player.webtorrent().isAutoResolutionForbidden()) return // Auto resolution if (resolutionId === -1) { - this.embed.player.peertube().enableAutoResolution() + this.embed.player.webtorrent().enableAutoResolution() return } - this.embed.player.peertube().disableAutoResolution() - this.embed.player.peertube().updateResolution(resolutionId) + this.embed.player.webtorrent().disableAutoResolution() + this.embed.player.webtorrent().updateResolution(resolutionId) } /** @@ -122,15 +118,17 @@ class PeerTubeEmbedApi { // PeerTube specific capabilities - this.embed.player.peertube().on('autoResolutionUpdate', () => this.loadResolutions()) - this.embed.player.peertube().on('videoFileUpdate', () => this.loadResolutions()) + if (this.embed.player.webtorrent) { + this.embed.player.webtorrent().on('autoResolutionUpdate', () => this.loadWebTorrentResolutions()) + this.embed.player.webtorrent().on('videoFileUpdate', () => this.loadWebTorrentResolutions()) + } } - private loadResolutions () { + private loadWebTorrentResolutions () { let resolutions = [] - let currentResolutionId = this.embed.player.peertube().getCurrentResolutionId() + let currentResolutionId = this.embed.player.webtorrent().getCurrentResolutionId() - for (const videoFile of this.embed.player.peertube().videoFiles) { + for (const videoFile of this.embed.player.webtorrent().videoFiles) { let label = videoFile.resolution.label if (videoFile.fps && videoFile.fps >= 50) { label += videoFile.fps @@ -266,9 +264,8 @@ class PeerTubeEmbed { const urlParts = window.location.pathname.split('/') const videoId = urlParts[ urlParts.length - 1 ] - const [ , serverTranslations, videoResponse, captionsResponse ] = await Promise.all([ - loadLocaleInVideoJS(window.location.origin, vjs, navigator.language), - getServerTranslations(window.location.origin, navigator.language), + const [ serverTranslations, videoResponse, captionsResponse ] = await Promise.all([ + PeertubePlayerManager.getServerTranslations(window.location.origin, navigator.language), this.loadVideoInfo(videoId), this.loadVideoCaptions(videoId) ]) @@ -292,43 +289,56 @@ class PeerTubeEmbed { this.loadParams() - const videojsOptions = getVideojsOptions({ - autoplay: this.autoplay, - controls: this.controls, - muted: this.muted, - loop: this.loop, - startTime: this.startTime, - subtitle: this.subtitle, - - videoCaptions, - inactivityTimeout: 1500, - videoViewUrl: this.getVideoUrl(videoId) + '/views', - playerElement: this.videoElement, - videoFiles: videoInfo.files, - videoDuration: videoInfo.duration, - enableHotkeys: true, - peertubeLink: true, - poster: window.location.origin + videoInfo.previewPath, - theaterMode: false - }) + const options: PeertubePlayerManagerOptions = { + common: { + autoplay: this.autoplay, + controls: this.controls, + muted: this.muted, + loop: this.loop, + captions: videoCaptions.length !== 0, + startTime: this.startTime, + subtitle: this.subtitle, + + videoCaptions, + inactivityTimeout: 1500, + videoViewUrl: this.getVideoUrl(videoId) + '/views', + playerElement: this.videoElement, + videoDuration: videoInfo.duration, + enableHotkeys: true, + peertubeLink: true, + poster: window.location.origin + videoInfo.previewPath, + theaterMode: false, + + serverUrl: window.location.origin, + language: navigator.language, + embedUrl: window.location.origin + videoInfo.embedPath + }, + + webtorrent: { + videoFiles: videoInfo.files + } + + // p2pMediaLoader: { + // // playlistUrl: 'https://akamai-axtest.akamaized.net/routes/lapd-v1-acceptance/www_c4/Manifest.m3u8' + // // playlistUrl: 'https://d2zihajmogu5jn.cloudfront.net/bipbop-advanced/bipbop_16x9_variant.m3u8' + // playlistUrl: 'https://cdn.theoplayer.com/video/elephants-dream/playlist.m3u8' + // } + } - this.playerOptions = videojsOptions - this.player = vjs(this.videoContainerId, videojsOptions, () => { - this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations)) + this.player = await PeertubePlayerManager.initialize('webtorrent', options) - window[ 'videojsPlayer' ] = this.player + this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations)) - if (this.controls) { - this.player.dock({ - title: videoInfo.name, - description: this.player.localize('Uses P2P, others may know your IP is downloading this video.') - }) - } + window[ 'videojsPlayer' ] = this.player - addContextMenu(this.player, window.location.origin + videoInfo.embedPath) + if (this.controls) { + this.player.dock({ + title: videoInfo.name, + description: this.player.localize('Uses P2P, others may know your IP is downloading this video.') + }) + } - this.initializeApi() - }) + this.initializeApi() } private handleError (err: Error, translations?: { [ id: string ]: string }) { diff --git a/client/src/tsconfig.app.json b/client/src/tsconfig.app.json index af7a74e9e..729eee353 100644 --- a/client/src/tsconfig.app.json +++ b/client/src/tsconfig.app.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "../out-tsc/app", "baseUrl": "./", - "module": "es2015", + "module": "esnext", "types": [], "lib": [ "es2017", diff --git a/client/yarn.lock b/client/yarn.lock index dee67c414..0698ca501 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -394,6 +394,11 @@ semver "5.5.1" semver-intersect "1.4.0" +"@streamroot/videojs-hlsjs-plugin@^1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@streamroot/videojs-hlsjs-plugin/-/videojs-hlsjs-plugin-1.0.7.tgz#581aecdf6a966162b404c60bd3ab8264eb89d334" + integrity sha512-7oAIOhEFxkfLOYWDfg7Oh3+OrnoTElRvUE3Jblg2B+SHmnrw4YXQnAwYJ0AHjNIBKoHnQubzZGttLaHAFJVspQ== + "@types/bittorrent-protocol@*": version "2.2.2" resolved "https://registry.yarnpkg.com/@types/bittorrent-protocol/-/bittorrent-protocol-2.2.2.tgz#169e9633e1bd18e6b830d11cf42e611b1972cb83" @@ -1445,7 +1450,7 @@ bittorrent-protocol@^3.0.0: unordered-array-remove "^1.0.2" xtend "^4.0.0" -bittorrent-tracker@^9.0.0: +bittorrent-tracker@^9.0.0, bittorrent-tracker@^9.10.1: version "9.10.1" resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.10.1.tgz#5de14aac012a287af394d3cc9eda1ec6cc956f11" integrity sha512-n5zTL/g6Wt0rb2EnkiyiaGYhth7I/N0/xMqGUpvGX/7g1scDGBVPhJnXR8lfp3/OMj681fv40o4q/otECMtZSA== @@ -3305,6 +3310,11 @@ events@^1.0.0: resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= +events@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88" + integrity sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA== + eventsource@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" @@ -3900,7 +3910,7 @@ genfun@^5.0.0: resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" integrity sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA== -get-browser-rtc@^1.0.0: +get-browser-rtc@^1.0.0, get-browser-rtc@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/get-browser-rtc/-/get-browser-rtc-1.0.2.tgz#bbcd40c8451a7ed4ef5c373b8169a409dd1d11d9" integrity sha1-u81AyEUaftTvXDc7gWmkCd0dEdk= @@ -6108,6 +6118,13 @@ m3u8-parser@4.2.0: resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.2.0.tgz#c8e0785fd17f741f4408b49466889274a9e36447" integrity sha512-LVHw0U6IPJjwk9i9f7Xe26NqaUHTNlIt4SSWoEfYFROeVKHN6MIjOhbRheI3dg8Jbq5WCuMFQ0QU3EgZpmzFPg== +m3u8-parser@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.3.0.tgz#4b4e988f87b6d8b2401d209a1d17798285a9da04" + integrity sha512-bVbjuBMoVIgFL1vpXVIxjeaoB5TPDJRb0m5qiTdM738SGqv/LAmsnVVPlKjM4fulm/rr1XZsKM+owHm+zvqxYA== + dependencies: + global "^4.3.2" + magic-string@^0.25.0: version "0.25.1" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.1.tgz#b1c248b399cd7485da0fe7385c2fc7011843266e" @@ -7214,6 +7231,26 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ== +p2p-media-loader-core@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/p2p-media-loader-core/-/p2p-media-loader-core-0.3.0.tgz#75687d7d7bee835d5c6c2f17d346add2dbe43b83" + integrity sha512-WKB9ONdA0kDQHXr6nixIL8t0UZuTD9Pqi/BIuaTiPUGDwYXUS/Mf5YynLAUupniLkIaDYD7/jmSLWqpZUDsAyw== + dependencies: + bittorrent-tracker "^9.10.1" + debug "^4.1.0" + events "^3.0.0" + get-browser-rtc "^1.0.2" + sha.js "^2.4.11" + +p2p-media-loader-hlsjs@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/p2p-media-loader-hlsjs/-/p2p-media-loader-hlsjs-0.3.0.tgz#4ee15d4d1a23aa0322a5be2bc6c329b6c913028d" + integrity sha512-U7PzMG5X7CVQ15OtMPRQjW68Msu0fuw8Pp0PRznX5uK0p26tSYMT/ZYCNeYCoDg3wGgJHM+327ed3M7TRJ4lcw== + dependencies: + events "^3.0.0" + m3u8-parser "^4.2.0" + p2p-media-loader-core "^0.3.0" + package-json-versionify@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/package-json-versionify/-/package-json-versionify-1.0.4.tgz#5860587a944873a6b7e6d26e8e51ffb22315bf17" @@ -8699,7 +8736,7 @@ setprototypeof@1.1.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== -sha.js@^2.4.0, sha.js@^2.4.8: +sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8: version "2.4.11" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== @@ -10090,6 +10127,14 @@ videojs-contextmenu-ui@^5.0.0: global "^4.3.2" video.js "^6 || ^7" +videojs-contrib-quality-levels@^2.0.9: + version "2.0.9" + resolved "https://registry.yarnpkg.com/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-2.0.9.tgz#b5d533d5092a6fc7d29eae1b43e4597d89bd527b" + integrity sha512-HJeaJJQdSufi9Y5T7jlyyhkeq+mWPCog86q6ypoTi66boBMMJTo2abiOSHS9KaOGAJjH72gfvrjVY5FRdjlxYA== + dependencies: + global "^4.3.2" + video.js "^6 || ^7" + videojs-dock@^2.0.2: version "2.1.4" resolved "https://registry.yarnpkg.com/videojs-dock/-/videojs-dock-2.1.4.tgz#0ebd198b5d48990e3523fdc87dbfdb9fe96f804c" diff --git a/scripts/dev/server.sh b/scripts/dev/server.sh index 9b8fddac6..b4675c57f 100755 --- a/scripts/dev/server.sh +++ b/scripts/dev/server.sh @@ -4,7 +4,7 @@ set -eu if [ ! -f "./client/dist/en_US/index.html" ]; then echo "client/dist/en_US/index.html does not exist, compile client files..." - npm run build:client + npm run build:client -- --light fi npm run watch:server diff --git a/server/middlewares/csp.ts b/server/middlewares/csp.ts index 8b919af0d..5fa9d1ab5 100644 --- a/server/middlewares/csp.ts +++ b/server/middlewares/csp.ts @@ -16,7 +16,7 @@ const baseDirectives = Object.assign({}, baseUri: ["'self'"], manifestSrc: ["'self'"], frameSrc: ["'self'"], // instead of deprecated child-src / self because of test-embed - workerSrc: ["'self'"] // instead of deprecated child-src + workerSrc: ["'self'", 'blob:'] // instead of deprecated child-src }, CONFIG.SERVICES['CSP-LOGGER'] ? { reportUri: CONFIG.SERVICES['CSP-LOGGER'] } : {}, CONFIG.WEBSERVER.SCHEME === 'https' ? { upgradeInsecureRequests: true } : {} -- 2.25.1