Refractor videojs player
Add fake p2p-media-loader plugin
This commit is contained in:
parent
7eeb6a0ba4
commit
2adfc7ea9a
|
@ -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",
|
||||
|
|
|
@ -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({
|
||||
const options = {
|
||||
common: {
|
||||
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,
|
||||
inactivityTimeout: 2500,
|
||||
poster: this.video.previewUrl,
|
||||
startTime,
|
||||
subtitle: urlOptions.subtitle,
|
||||
|
||||
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,
|
||||
|
||||
userWatching: this.user && this.user.videosHistoryEnabled === true ? {
|
||||
url: this.videoService.getUserWatchingVideoUrl(this.video.uuid),
|
||||
authorizationHeader: this.authService.getRequestHeaderValue()
|
||||
} : undefined
|
||||
})
|
||||
} : undefined,
|
||||
|
||||
if (this.videojsLocaleLoaded === false) {
|
||||
await loadLocaleInVideoJS(environment.apiUrl, videojs, isOnDevLocale() ? getDevLocale() : this.localeId)
|
||||
this.videojsLocaleLoaded = true
|
||||
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()
|
||||
|
|
|
@ -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 }
|
|
@ -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<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
|
||||
}
|
|
@ -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<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
|
||||
}
|
|
@ -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 }
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 + ' <small>' + this.player_.peertube().getCurrentResolutionLabel() + '</small>'
|
||||
}
|
||||
|
||||
return this.label
|
||||
}
|
||||
}
|
||||
MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
|
||||
|
||||
export { ResolutionMenuItem }
|
|
@ -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)
|
|
@ -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'
|
|
@ -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) + '%'
|
|
@ -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,39 +38,7 @@ 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
|
||||
}
|
||||
|
||||
menu.addChild(new ResolutionMenuItem(
|
||||
this.player_,
|
||||
{
|
||||
id: videoFile.resolution.id,
|
||||
label,
|
||||
src: videoFile.magnetUri
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
menu.addChild(new ResolutionMenuItem(
|
||||
this.player_,
|
||||
{
|
||||
id: -1,
|
||||
label: this.player_.localize('Auto'),
|
||||
src: null
|
||||
}
|
||||
))
|
||||
|
||||
return menu
|
||||
}
|
||||
|
||||
updateLabel () {
|
||||
if (!this.labelEl_) return
|
||||
|
||||
this.labelEl_.innerHTML = this.buildLabelHTML()
|
||||
return new Menu(this.player_)
|
||||
}
|
||||
|
||||
buildCSSClass () {
|
||||
|
@ -79,8 +49,34 @@ class ResolutionMenuButton extends MenuButton {
|
|||
return 'vjs-resolution-control ' + super.buildWrapperCSSClass()
|
||||
}
|
||||
|
||||
private buildLabelHTML () {
|
||||
return this.player_.peertube().getCurrentResolutionLabel()
|
||||
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: d.id,
|
||||
label: d.label,
|
||||
selected: d.selected,
|
||||
callback: data.qualitySwitchCallback
|
||||
})
|
||||
)
|
||||
|
||||
labels[d.id] = d.label
|
||||
}
|
||||
|
||||
this.menu.addChild(new ResolutionMenuItem(
|
||||
this.player_,
|
||||
{
|
||||
id: -1,
|
||||
label: this.player_.localize('Auto'),
|
||||
labels,
|
||||
callback: data.qualitySwitchCallback,
|
||||
selected: true // By default, in auto mode
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
ResolutionMenuButton.prototype.controlText_ = 'Quality'
|
|
@ -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 + ' <small>' + this.currentResolutionLabel + '</small>'
|
||||
}
|
||||
|
||||
return this.label
|
||||
}
|
||||
}
|
||||
MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
|
||||
|
||||
export { ResolutionMenuItem }
|
|
@ -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')
|
|
@ -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_)
|
|
@ -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 {
|
|
@ -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.trigger('captionsChanged')
|
||||
this.player.qualityLevels().addQualityLevel(representation)
|
||||
|
||||
qualityLevelsPayload.push({
|
||||
id: representation.id,
|
||||
label: representation.label,
|
||||
selected: false
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
const payload: LoadedQualityData = {
|
||||
qualitySwitchCallback: (d: any) => this.qualitySwitchCallback(d),
|
||||
qualityData: {
|
||||
video: qualityLevelsPayload
|
||||
}
|
||||
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()
|
||||
this.player.trigger('loadedqualitydata', payload)
|
||||
}
|
||||
|
||||
private buildQualityLabel (file: VideoFile) {
|
||||
let label = file.resolution.label
|
||||
|
||||
if (file.fps && file.fps >= 50) {
|
||||
label += file.fps
|
||||
}
|
||||
|
||||
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 }
|
|
@ -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,11 +289,13 @@ class PeerTubeEmbed {
|
|||
|
||||
this.loadParams()
|
||||
|
||||
const videojsOptions = getVideojsOptions({
|
||||
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,
|
||||
|
||||
|
@ -304,16 +303,30 @@ class PeerTubeEmbed {
|
|||
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
|
||||
})
|
||||
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.player = await PeertubePlayerManager.initialize('webtorrent', options)
|
||||
|
||||
this.playerOptions = videojsOptions
|
||||
this.player = vjs(this.videoContainerId, videojsOptions, () => {
|
||||
this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations))
|
||||
|
||||
window[ 'videojsPlayer' ] = this.player
|
||||
|
@ -325,10 +338,7 @@ class PeerTubeEmbed {
|
|||
})
|
||||
}
|
||||
|
||||
addContextMenu(this.player, window.location.origin + videoInfo.embedPath)
|
||||
|
||||
this.initializeApi()
|
||||
})
|
||||
}
|
||||
|
||||
private handleError (err: Error, translations?: { [ id: string ]: string }) {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/app",
|
||||
"baseUrl": "./",
|
||||
"module": "es2015",
|
||||
"module": "esnext",
|
||||
"types": [],
|
||||
"lib": [
|
||||
"es2017",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 } : {}
|
||||
|
|
Loading…
Reference in New Issue