"@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",
"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",
"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",
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'
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',
@ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent
@ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
- player: videojs.Player
+ player: any
playerElement: HTMLVideoElement
userRating: UserVideoRateType = null
video: VideoDetails = null
remoteServerDown = false
hotkeys: Hotkey[]
- private videojsLocaleLoaded = false
private paramsSub: Subscription
constructor (
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()
--- /dev/null
+// 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 }
--- /dev/null
+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<any>
+
+ 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
+}
+++ /dev/null
-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<any>
-
- 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
-}
--- /dev/null
+// 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 }
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
}
}
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
}
+++ /dev/null
-// 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 + ' <small>' + this.player_.peertube().getCurrentResolutionLabel() + '</small>'
- }
-
- return this.label
- }
-}
-MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
-
-export { ResolutionMenuItem }
-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', {
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'
return div
}
}
-Button.registerComponent('WebTorrentButton', WebtorrentInfoButton)
+Button.registerComponent('P2PInfoButton', P2pInfoButton)
-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'
-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'
}
update () {
- const torrent = this.player().peertube().getTorrent()
+ const torrent = this.player().webtorrent().getTorrent()
if (!torrent) return
this.el_.style.width = (torrent.progress * 100) + '%'
// @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')
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_)
}
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'
--- /dev/null
+// 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 + ' <small>' + this.currentResolutionLabel + '</small>'
+ }
+
+ return this.label
+ }
+}
+MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
+
+export { ResolutionMenuItem }
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')
// @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')
}
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_)
// @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 {
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')
}
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
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({
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_
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
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)
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: {
return done()
})
- this.trigger('videoFileUpdate')
+ this.changeQuality()
+ this.trigger('videoFileUpdate', { auto: this.autoResolution, resolutionId: this.currentVideoFile.resolution.id })
}
updateResolution (resolutionId: number, delay = 0) {
}
}
- 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 () {
}
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()
// 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()
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,
}, 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()
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)
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]
}
}
- 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 }
// 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
}
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)
}
/**
// 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
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)
])
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 }) {
"compilerOptions": {
"outDir": "../out-tsc/app",
"baseUrl": "./",
- "module": "es2015",
+ "module": "esnext",
"types": [],
"lib": [
"es2017",
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"
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==
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"
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=
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"
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"
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==
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"
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
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 } : {}