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-loading-bar/router": "^3.0.0",
|
||||||
"@ngx-meta/core": "^6.0.0-rc.1",
|
"@ngx-meta/core": "^6.0.0-rc.1",
|
||||||
"@ngx-translate/i18n-polyfill": "^1.0.0",
|
"@ngx-translate/i18n-polyfill": "^1.0.0",
|
||||||
|
"@streamroot/videojs-hlsjs-plugin": "^1.0.7",
|
||||||
"@types/core-js": "^2.5.0",
|
"@types/core-js": "^2.5.0",
|
||||||
"@types/jasmine": "^2.8.7",
|
"@types/jasmine": "^2.8.7",
|
||||||
"@types/jasminewd2": "^2.0.3",
|
"@types/jasminewd2": "^2.0.3",
|
||||||
|
@ -131,6 +132,7 @@
|
||||||
"ngx-qrcode2": "^0.0.9",
|
"ngx-qrcode2": "^0.0.9",
|
||||||
"node-sass": "^4.9.3",
|
"node-sass": "^4.9.3",
|
||||||
"npm-font-source-sans-pro": "^1.0.2",
|
"npm-font-source-sans-pro": "^1.0.2",
|
||||||
|
"p2p-media-loader-hlsjs": "^0.3.0",
|
||||||
"path-browserify": "^1.0.0",
|
"path-browserify": "^1.0.0",
|
||||||
"primeng": "^7.0.0",
|
"primeng": "^7.0.0",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
|
@ -152,6 +154,7 @@
|
||||||
"typescript": "3.1.6",
|
"typescript": "3.1.6",
|
||||||
"video.js": "^7",
|
"video.js": "^7",
|
||||||
"videojs-contextmenu-ui": "^5.0.0",
|
"videojs-contextmenu-ui": "^5.0.0",
|
||||||
|
"videojs-contrib-quality-levels": "^2.0.9",
|
||||||
"videojs-dock": "^2.0.2",
|
"videojs-dock": "^2.0.2",
|
||||||
"videojs-hotkeys": "^0.2.21",
|
"videojs-hotkeys": "^0.2.21",
|
||||||
"webpack-bundle-analyzer": "^3.0.2",
|
"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 { MetaService } from '@ngx-meta/core'
|
||||||
import { Notifier, ServerService } from '@app/core'
|
import { Notifier, ServerService } from '@app/core'
|
||||||
import { forkJoin, Subscription } from 'rxjs'
|
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 { Hotkey, HotkeysService } from 'angular2-hotkeys'
|
||||||
import * as WebTorrent from 'webtorrent'
|
import * as WebTorrent from 'webtorrent'
|
||||||
import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared'
|
import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared'
|
||||||
import '../../../assets/player/peertube-videojs-plugin'
|
|
||||||
import { AuthService, ConfirmService } from '../../core'
|
import { AuthService, ConfirmService } from '../../core'
|
||||||
import { RestExtractor, VideoBlacklistService } from '../../shared'
|
import { RestExtractor, VideoBlacklistService } from '../../shared'
|
||||||
import { VideoDetails } from '../../shared/video/video-details.model'
|
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 { VideoShareComponent } from './modal/video-share.component'
|
||||||
import { VideoBlacklistComponent } from './modal/video-blacklist.component'
|
import { VideoBlacklistComponent } from './modal/video-blacklist.component'
|
||||||
import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.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 { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { environment } from '../../../environments/environment'
|
import { environment } from '../../../environments/environment'
|
||||||
import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
|
|
||||||
import { VideoCaptionService } from '@app/shared/video-caption'
|
import { VideoCaptionService } from '@app/shared/video-caption'
|
||||||
import { MarkdownService } from '@app/shared/renderer'
|
import { MarkdownService } from '@app/shared/renderer'
|
||||||
|
import { PeertubePlayerManager } from '../../../assets/player/peertube-player-manager'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-watch',
|
selector: 'my-video-watch',
|
||||||
|
@ -46,7 +40,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
@ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent
|
@ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent
|
||||||
@ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
|
@ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
|
||||||
|
|
||||||
player: videojs.Player
|
player: any
|
||||||
playerElement: HTMLVideoElement
|
playerElement: HTMLVideoElement
|
||||||
userRating: UserVideoRateType = null
|
userRating: UserVideoRateType = null
|
||||||
video: VideoDetails = null
|
video: VideoDetails = null
|
||||||
|
@ -61,7 +55,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
remoteServerDown = false
|
remoteServerDown = false
|
||||||
hotkeys: Hotkey[]
|
hotkeys: Hotkey[]
|
||||||
|
|
||||||
private videojsLocaleLoaded = false
|
|
||||||
private paramsSub: Subscription
|
private paramsSub: Subscription
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
|
@ -402,41 +395,45 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
src: environment.apiUrl + c.captionPath
|
src: environment.apiUrl + c.captionPath
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const videojsOptions = getVideojsOptions({
|
const options = {
|
||||||
autoplay: this.isAutoplay(),
|
common: {
|
||||||
inactivityTimeout: 2500,
|
autoplay: this.isAutoplay(),
|
||||||
videoFiles: this.video.files,
|
playerElement: this.playerElement,
|
||||||
videoCaptions: playerCaptions,
|
videoDuration: this.video.duration,
|
||||||
playerElement: this.playerElement,
|
enableHotkeys: true,
|
||||||
videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null,
|
inactivityTimeout: 2500,
|
||||||
videoDuration: this.video.duration,
|
poster: this.video.previewUrl,
|
||||||
enableHotkeys: true,
|
startTime,
|
||||||
peertubeLink: false,
|
|
||||||
poster: this.video.previewUrl,
|
|
||||||
startTime,
|
|
||||||
subtitle: urlOptions.subtitle,
|
|
||||||
theaterMode: true,
|
|
||||||
language: this.localeId,
|
|
||||||
|
|
||||||
userWatching: this.user && this.user.videosHistoryEnabled === true ? {
|
theaterMode: true,
|
||||||
url: this.videoService.getUserWatchingVideoUrl(this.video.uuid),
|
captions: videoCaptions.length !== 0,
|
||||||
authorizationHeader: this.authService.getRequestHeaderValue()
|
peertubeLink: false,
|
||||||
} : undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.videojsLocaleLoaded === false) {
|
videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null,
|
||||||
await loadLocaleInVideoJS(environment.apiUrl, videojs, isOnDevLocale() ? getDevLocale() : this.localeId)
|
embedUrl: this.video.embedUrl,
|
||||||
this.videojsLocaleLoaded = true
|
|
||||||
|
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,
|
||||||
|
|
||||||
|
serverUrl: environment.apiUrl,
|
||||||
|
|
||||||
|
videoCaptions: playerCaptions
|
||||||
|
},
|
||||||
|
|
||||||
|
webtorrent: {
|
||||||
|
videoFiles: this.video.files
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const self = this
|
|
||||||
this.zone.runOutsideAngular(async () => {
|
this.zone.runOutsideAngular(async () => {
|
||||||
videojs(this.playerElement, videojsOptions, function (this: videojs.Player) {
|
this.player = await PeertubePlayerManager.initialize('webtorrent', options)
|
||||||
self.player = this
|
this.player.on('customError', ({ err }: { err: any }) => this.handleError(err))
|
||||||
this.on('customError', ({ err }: { err: any }) => self.handleError(err))
|
|
||||||
|
|
||||||
addContextMenu(self.player, self.video.embedUrl)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.setVideoDescriptionHTML()
|
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 * as videojs from 'video.js'
|
||||||
|
|
||||||
import { VideoFile } from '../../../../shared/models/videos/video.model'
|
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 {
|
declare namespace videojs {
|
||||||
interface Player {
|
interface Player {
|
||||||
peertube (): PeerTubePlugin
|
peertube (): PeerTubePlugin
|
||||||
|
webtorrent (): WebTorrentPlugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,26 +32,73 @@ type UserWatching = {
|
||||||
authorizationHeader: string
|
authorizationHeader: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type PeertubePluginOptions = {
|
type PeerTubePluginOptions = {
|
||||||
videoFiles: VideoFile[]
|
autoplay: boolean
|
||||||
playerElement: HTMLVideoElement
|
|
||||||
videoViewUrl: string
|
videoViewUrl: string
|
||||||
videoDuration: number
|
videoDuration: number
|
||||||
startTime: number | string
|
startTime: number | string
|
||||||
autoplay: boolean,
|
|
||||||
videoCaptions: VideoJSCaption[]
|
|
||||||
|
|
||||||
subtitle?: string
|
|
||||||
userWatching?: UserWatching
|
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
|
// videojs typings don't have some method we need
|
||||||
const videojsUntyped = videojs as any
|
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 {
|
export {
|
||||||
|
ResolutionUpdateData,
|
||||||
|
AutoResolutionUpdateData,
|
||||||
VideoJSComponentInterface,
|
VideoJSComponentInterface,
|
||||||
PeertubePluginOptions,
|
|
||||||
videojsUntyped,
|
videojsUntyped,
|
||||||
VideoJSCaption,
|
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 { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
|
||||||
import { bytes } from './utils'
|
import { bytes } from '../utils'
|
||||||
|
|
||||||
const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
|
const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
|
||||||
class WebtorrentInfoButton extends Button {
|
class P2pInfoButton extends Button {
|
||||||
|
|
||||||
createEl () {
|
createEl () {
|
||||||
const div = videojsUntyped.dom.createEl('div', {
|
const div = videojsUntyped.dom.createEl('div', {
|
||||||
|
@ -65,7 +65,7 @@ class WebtorrentInfoButton extends Button {
|
||||||
subDivHttp.appendChild(subDivHttpText)
|
subDivHttp.appendChild(subDivHttpText)
|
||||||
div.appendChild(subDivHttp)
|
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
|
// We are in HTTP fallback
|
||||||
if (!data) {
|
if (!data) {
|
||||||
subDivHttp.className = 'vjs-peertube-displayed'
|
subDivHttp.className = 'vjs-peertube-displayed'
|
||||||
|
@ -99,4 +99,4 @@ class WebtorrentInfoButton extends Button {
|
||||||
return div
|
return div
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Button.registerComponent('WebTorrentButton', WebtorrentInfoButton)
|
Button.registerComponent('P2PInfoButton', P2pInfoButton)
|
|
@ -1,5 +1,5 @@
|
||||||
import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
|
import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
|
||||||
import { buildVideoLink } from './utils'
|
import { buildVideoLink } from '../utils'
|
||||||
// FIXME: something weird with our path definition in tsconfig and typings
|
// FIXME: something weird with our path definition in tsconfig and typings
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { Player } from 'video.js'
|
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
|
// FIXME: something weird with our path definition in tsconfig and typings
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { Player } from 'video.js'
|
import { Player } from 'video.js'
|
||||||
|
@ -27,7 +27,7 @@ class PeerTubeLoadProgressBar extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
update () {
|
update () {
|
||||||
const torrent = this.player().peertube().getTorrent()
|
const torrent = this.player().webtorrent().getTorrent()
|
||||||
if (!torrent) return
|
if (!torrent) return
|
||||||
|
|
||||||
this.el_.style.width = (torrent.progress * 100) + '%'
|
this.el_.style.width = (torrent.progress * 100) + '%'
|
|
@ -2,7 +2,7 @@
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { Player } from 'video.js'
|
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'
|
import { ResolutionMenuItem } from './resolution-menu-item'
|
||||||
|
|
||||||
const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
|
const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
|
||||||
|
@ -14,16 +14,18 @@ class ResolutionMenuButton extends MenuButton {
|
||||||
super(player, options)
|
super(player, options)
|
||||||
this.player = player
|
this.player = player
|
||||||
|
|
||||||
player.peertube().on('videoFileUpdate', () => this.updateLabel())
|
player.on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data))
|
||||||
player.peertube().on('autoResolutionUpdate', () => this.updateLabel())
|
|
||||||
|
if (player.webtorrent) {
|
||||||
|
player.webtorrent().on('videoFileUpdate', () => setTimeout(() => this.trigger('updateLabel'), 0))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createEl () {
|
createEl () {
|
||||||
const el = super.createEl()
|
const el = super.createEl()
|
||||||
|
|
||||||
this.labelEl_ = videojsUntyped.dom.createEl('div', {
|
this.labelEl_ = videojsUntyped.dom.createEl('div', {
|
||||||
className: 'vjs-resolution-value',
|
className: 'vjs-resolution-value'
|
||||||
innerHTML: this.buildLabelHTML()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
el.appendChild(this.labelEl_)
|
el.appendChild(this.labelEl_)
|
||||||
|
@ -36,39 +38,7 @@ class ResolutionMenuButton extends MenuButton {
|
||||||
}
|
}
|
||||||
|
|
||||||
createMenu () {
|
createMenu () {
|
||||||
const menu = new Menu(this.player_)
|
return 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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildCSSClass () {
|
buildCSSClass () {
|
||||||
|
@ -79,8 +49,34 @@ class ResolutionMenuButton extends MenuButton {
|
||||||
return 'vjs-resolution-control ' + super.buildWrapperCSSClass()
|
return 'vjs-resolution-control ' + super.buildWrapperCSSClass()
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildLabelHTML () {
|
private buildQualities (data: LoadedQualityData) {
|
||||||
return this.player_.peertube().getCurrentResolutionLabel()
|
// 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'
|
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 * as videojs from 'video.js'
|
||||||
|
|
||||||
import { SettingsMenuItem } from './settings-menu-item'
|
import { SettingsMenuItem } from './settings-menu-item'
|
||||||
import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
|
import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
|
||||||
import { toTitleCase } from './utils'
|
import { toTitleCase } from '../utils'
|
||||||
|
|
||||||
const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
|
const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
|
||||||
const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
|
const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
|
|
@ -5,8 +5,8 @@
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import * as videojs from 'video.js'
|
import * as videojs from 'video.js'
|
||||||
|
|
||||||
import { toTitleCase } from './utils'
|
import { toTitleCase } from '../utils'
|
||||||
import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
|
import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
|
||||||
|
|
||||||
const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
|
const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
|
||||||
const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component')
|
const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component')
|
||||||
|
@ -220,12 +220,9 @@ class SettingsMenuItem extends MenuItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
build () {
|
build () {
|
||||||
const saveUpdateLabel = this.subMenu.updateLabel
|
this.subMenu.on('updateLabel', () => {
|
||||||
this.subMenu.updateLabel = () => {
|
|
||||||
this.update()
|
this.update()
|
||||||
|
})
|
||||||
saveUpdateLabel.call(this.subMenu)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_)
|
this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_)
|
||||||
this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_)
|
this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_)
|
|
@ -2,8 +2,8 @@
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import * as videojs from 'video.js'
|
import * as videojs from 'video.js'
|
||||||
|
|
||||||
import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
|
import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
|
||||||
import { saveTheaterInStore, getStoredTheater } from './peertube-player-local-storage'
|
import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage'
|
||||||
|
|
||||||
const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
|
const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
|
||||||
class TheaterButton extends Button {
|
class TheaterButton extends Button {
|
|
@ -4,21 +4,16 @@ import * as videojs from 'video.js'
|
||||||
|
|
||||||
import * as WebTorrent from 'webtorrent'
|
import * as WebTorrent from 'webtorrent'
|
||||||
import { VideoFile } from '../../../../shared/models/videos/video.model'
|
import { VideoFile } from '../../../../shared/models/videos/video.model'
|
||||||
import { renderVideo } from './video-renderer'
|
import { renderVideo } from './webtorrent/video-renderer'
|
||||||
import './settings-menu-button'
|
import { LoadedQualityData, VideoJSComponentInterface, WebtorrentPluginOptions } from './peertube-videojs-typings'
|
||||||
import { PeertubePluginOptions, UserWatching, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
|
import { videoFileMaxByResolution, videoFileMinByResolution } from './utils'
|
||||||
import { isMobile, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from './utils'
|
import { PeertubeChunkStore } from './webtorrent/peertube-chunk-store'
|
||||||
import { PeertubeChunkStore } from './peertube-chunk-store'
|
|
||||||
import {
|
import {
|
||||||
getAverageBandwidthInStore,
|
getAverageBandwidthInStore,
|
||||||
getStoredLastSubtitle,
|
|
||||||
getStoredMute,
|
getStoredMute,
|
||||||
getStoredVolume,
|
getStoredVolume,
|
||||||
getStoredWebTorrentEnabled,
|
getStoredWebTorrentEnabled,
|
||||||
saveAverageBandwidth,
|
saveAverageBandwidth
|
||||||
saveLastSubtitle,
|
|
||||||
saveMuteInStore,
|
|
||||||
saveVolumeInStore
|
|
||||||
} from './peertube-player-local-storage'
|
} from './peertube-player-local-storage'
|
||||||
|
|
||||||
const CacheChunkStore = require('cache-chunk-store')
|
const CacheChunkStore = require('cache-chunk-store')
|
||||||
|
@ -30,14 +25,13 @@ type PlayOptions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
|
const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
|
||||||
class PeerTubePlugin extends Plugin {
|
class WebTorrentPlugin extends Plugin {
|
||||||
private readonly playerElement: HTMLVideoElement
|
private readonly playerElement: HTMLVideoElement
|
||||||
|
|
||||||
private readonly autoplay: boolean = false
|
private readonly autoplay: boolean = false
|
||||||
private readonly startTime: number = 0
|
private readonly startTime: number = 0
|
||||||
private readonly savePlayerSrcFunction: Function
|
private readonly savePlayerSrcFunction: Function
|
||||||
private readonly videoFiles: VideoFile[]
|
private readonly videoFiles: VideoFile[]
|
||||||
private readonly videoViewUrl: string
|
|
||||||
private readonly videoDuration: number
|
private readonly videoDuration: number
|
||||||
private readonly CONSTANTS = {
|
private readonly CONSTANTS = {
|
||||||
INFO_SCHEDULER: 1000, // Don't change this
|
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_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_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
|
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
|
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly webtorrent = new WebTorrent({
|
private readonly webtorrent = new WebTorrent({
|
||||||
|
@ -68,46 +61,37 @@ class PeerTubePlugin extends Plugin {
|
||||||
private player: any
|
private player: any
|
||||||
private currentVideoFile: VideoFile
|
private currentVideoFile: VideoFile
|
||||||
private torrent: WebTorrent.Torrent
|
private torrent: WebTorrent.Torrent
|
||||||
private videoCaptions: VideoJSCaption[]
|
|
||||||
private defaultSubtitle: string
|
|
||||||
|
|
||||||
private renderer: any
|
private renderer: any
|
||||||
private fakeRenderer: any
|
private fakeRenderer: any
|
||||||
private destroyingFakeRenderer = false
|
private destroyingFakeRenderer = false
|
||||||
|
|
||||||
private autoResolution = true
|
private autoResolution = true
|
||||||
private forbidAutoResolution = false
|
private autoResolutionPossible = true
|
||||||
private isAutoResolutionObservation = false
|
private isAutoResolutionObservation = false
|
||||||
private playerRefusedP2P = false
|
private playerRefusedP2P = false
|
||||||
|
|
||||||
private videoViewInterval: any
|
|
||||||
private torrentInfoInterval: any
|
private torrentInfoInterval: any
|
||||||
private autoQualityInterval: any
|
private autoQualityInterval: any
|
||||||
private userWatchingVideoInterval: any
|
|
||||||
private addTorrentDelay: any
|
private addTorrentDelay: any
|
||||||
private qualityObservationTimer: any
|
private qualityObservationTimer: any
|
||||||
private runAutoQualitySchedulerTimer: any
|
private runAutoQualitySchedulerTimer: any
|
||||||
|
|
||||||
private downloadSpeeds: number[] = []
|
private downloadSpeeds: number[] = []
|
||||||
|
|
||||||
constructor (player: videojs.Player, options: PeertubePluginOptions) {
|
constructor (player: videojs.Player, options: WebtorrentPluginOptions) {
|
||||||
super(player, options)
|
super(player, options)
|
||||||
|
|
||||||
// Disable auto play on iOS
|
// Disable auto play on iOS
|
||||||
this.autoplay = options.autoplay && this.isIOS() === false
|
this.autoplay = options.autoplay && this.isIOS() === false
|
||||||
this.playerRefusedP2P = !getStoredWebTorrentEnabled()
|
this.playerRefusedP2P = !getStoredWebTorrentEnabled()
|
||||||
|
|
||||||
this.startTime = timeToInt(options.startTime)
|
|
||||||
this.videoFiles = options.videoFiles
|
this.videoFiles = options.videoFiles
|
||||||
this.videoViewUrl = options.videoViewUrl
|
|
||||||
this.videoDuration = options.videoDuration
|
this.videoDuration = options.videoDuration
|
||||||
this.videoCaptions = options.videoCaptions
|
|
||||||
|
|
||||||
this.savePlayerSrcFunction = this.player.src
|
this.savePlayerSrcFunction = this.player.src
|
||||||
this.playerElement = options.playerElement
|
this.playerElement = options.playerElement
|
||||||
|
|
||||||
if (this.autoplay === true) this.player.addClass('vjs-has-autoplay')
|
|
||||||
|
|
||||||
this.player.ready(() => {
|
this.player.ready(() => {
|
||||||
const playerOptions = this.player.options_
|
const playerOptions = this.player.options_
|
||||||
|
|
||||||
|
@ -117,33 +101,10 @@ class PeerTubePlugin extends Plugin {
|
||||||
const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
|
const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
|
||||||
if (muted !== undefined) this.player.muted(muted)
|
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.player.duration(options.videoDuration)
|
||||||
|
|
||||||
this.initializePlayer()
|
this.initializePlayer()
|
||||||
this.runTorrentInfoScheduler()
|
this.runTorrentInfoScheduler()
|
||||||
this.runViewAdd()
|
|
||||||
|
|
||||||
if (options.userWatching) this.runUserWatchVideo(options.userWatching)
|
|
||||||
|
|
||||||
this.player.one('play', () => {
|
this.player.one('play', () => {
|
||||||
// Don't run immediately scheduler, wait some seconds the TCP connections are made
|
// 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.qualityObservationTimer)
|
||||||
clearTimeout(this.runAutoQualitySchedulerTimer)
|
clearTimeout(this.runAutoQualitySchedulerTimer)
|
||||||
|
|
||||||
clearInterval(this.videoViewInterval)
|
|
||||||
clearInterval(this.torrentInfoInterval)
|
clearInterval(this.torrentInfoInterval)
|
||||||
clearInterval(this.autoQualityInterval)
|
clearInterval(this.autoQualityInterval)
|
||||||
|
|
||||||
if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
|
|
||||||
|
|
||||||
// Don't need to destroy renderer, video player will be destroyed
|
// Don't need to destroy renderer, video player will be destroyed
|
||||||
this.flushVideoFile(this.currentVideoFile, false)
|
this.flushVideoFile(this.currentVideoFile, false)
|
||||||
|
|
||||||
|
@ -173,13 +131,6 @@ class PeerTubePlugin extends Plugin {
|
||||||
return this.currentVideoFile ? this.currentVideoFile.resolution.id : -1
|
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 (
|
updateVideoFile (
|
||||||
videoFile?: VideoFile,
|
videoFile?: VideoFile,
|
||||||
options: {
|
options: {
|
||||||
|
@ -228,7 +179,8 @@ class PeerTubePlugin extends Plugin {
|
||||||
return done()
|
return done()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.trigger('videoFileUpdate')
|
this.changeQuality()
|
||||||
|
this.trigger('videoFileUpdate', { auto: this.autoResolution, resolutionId: this.currentVideoFile.resolution.id })
|
||||||
}
|
}
|
||||||
|
|
||||||
updateResolution (resolutionId: number, delay = 0) {
|
updateResolution (resolutionId: number, delay = 0) {
|
||||||
|
@ -262,28 +214,17 @@ class PeerTubePlugin extends Plugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isAutoResolutionOn () {
|
|
||||||
return this.autoResolution
|
|
||||||
}
|
|
||||||
|
|
||||||
enableAutoResolution () {
|
enableAutoResolution () {
|
||||||
this.autoResolution = true
|
this.autoResolution = true
|
||||||
this.trigger('autoResolutionUpdate')
|
this.trigger('videoFileUpdate', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() })
|
||||||
}
|
}
|
||||||
|
|
||||||
disableAutoResolution (forbid = false) {
|
disableAutoResolution (forbid = false) {
|
||||||
if (forbid === true) this.forbidAutoResolution = true
|
if (forbid === true) this.autoResolutionPossible = false
|
||||||
|
|
||||||
this.autoResolution = false
|
this.autoResolution = false
|
||||||
this.trigger('autoResolutionUpdate')
|
this.trigger('autoResolutionUpdate', { possible: this.autoResolutionPossible })
|
||||||
}
|
this.trigger('videoFileUpdate', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() })
|
||||||
|
|
||||||
isAutoResolutionForbidden () {
|
|
||||||
return this.forbidAutoResolution === true
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentVideoFile () {
|
|
||||||
return this.currentVideoFile
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTorrent () {
|
getTorrent () {
|
||||||
|
@ -462,13 +403,7 @@ class PeerTubePlugin extends Plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializePlayer () {
|
private initializePlayer () {
|
||||||
if (isMobile()) this.player.addClass('vjs-is-mobile')
|
this.buildQualities()
|
||||||
|
|
||||||
this.initSmoothProgressBar()
|
|
||||||
|
|
||||||
this.initCaptions()
|
|
||||||
|
|
||||||
this.alterInactivity()
|
|
||||||
|
|
||||||
if (this.autoplay === true) {
|
if (this.autoplay === true) {
|
||||||
this.player.posterImage.hide()
|
this.player.posterImage.hide()
|
||||||
|
@ -491,7 +426,7 @@ class PeerTubePlugin extends Plugin {
|
||||||
|
|
||||||
// Not initialized or in HTTP fallback
|
// Not initialized or in HTTP fallback
|
||||||
if (this.torrent === undefined || this.torrent === null) return
|
if (this.torrent === undefined || this.torrent === null) return
|
||||||
if (this.isAutoResolutionOn() === false) return
|
if (this.autoResolution === false) return
|
||||||
if (this.isAutoResolutionObservation === true) return
|
if (this.isAutoResolutionObservation === true) return
|
||||||
|
|
||||||
const file = this.getAppropriateFile()
|
const file = this.getAppropriateFile()
|
||||||
|
@ -531,12 +466,12 @@ class PeerTubePlugin extends Plugin {
|
||||||
if (this.torrent === undefined) return
|
if (this.torrent === undefined) return
|
||||||
|
|
||||||
// Http fallback
|
// 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
|
// 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)
|
if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed)
|
||||||
|
|
||||||
return this.trigger('torrentInfo', {
|
return this.player.trigger('p2pInfo', {
|
||||||
downloadSpeed: this.torrent.downloadSpeed,
|
downloadSpeed: this.torrent.downloadSpeed,
|
||||||
numPeers: this.torrent.numPeers,
|
numPeers: this.torrent.numPeers,
|
||||||
uploadSpeed: this.torrent.uploadSpeed,
|
uploadSpeed: this.torrent.uploadSpeed,
|
||||||
|
@ -546,65 +481,6 @@ class PeerTubePlugin extends Plugin {
|
||||||
}, this.CONSTANTS.INFO_SCHEDULER)
|
}, 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) {
|
private fallbackToHttp (options: PlayOptions, done?: Function) {
|
||||||
const paused = this.player.paused()
|
const paused = this.player.paused()
|
||||||
|
|
||||||
|
@ -620,8 +496,10 @@ class PeerTubePlugin extends Plugin {
|
||||||
this.player.src = this.savePlayerSrcFunction
|
this.player.src = this.savePlayerSrcFunction
|
||||||
this.player.src(httpUrl)
|
this.player.src(httpUrl)
|
||||||
|
|
||||||
|
this.changeQuality()
|
||||||
|
|
||||||
// We changed the source, so reinit captions
|
// We changed the source, so reinit captions
|
||||||
this.initCaptions()
|
this.player.trigger('sourcechange')
|
||||||
|
|
||||||
return this.tryToPlay(err => {
|
return this.tryToPlay(err => {
|
||||||
if (err && done) return done(err)
|
if (err && done) return done(err)
|
||||||
|
@ -649,25 +527,6 @@ class PeerTubePlugin extends Plugin {
|
||||||
return !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform)
|
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 () {
|
private pickAverageVideoFile () {
|
||||||
if (this.videoFiles.length === 1) return this.videoFiles[0]
|
if (this.videoFiles.length === 1) return this.videoFiles[0]
|
||||||
|
|
||||||
|
@ -712,43 +571,70 @@ class PeerTubePlugin extends Plugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private initCaptions () {
|
private buildQualities () {
|
||||||
for (const caption of this.videoCaptions) {
|
const qualityLevelsPayload = []
|
||||||
this.player.addRemoteTextTrack({
|
|
||||||
kind: 'captions',
|
for (const file of this.videoFiles) {
|
||||||
label: caption.label,
|
const representation = {
|
||||||
language: caption.language,
|
id: file.resolution.id,
|
||||||
id: caption.language,
|
label: this.buildQualityLabel(file),
|
||||||
src: caption.src,
|
height: file.resolution.id,
|
||||||
default: this.defaultSubtitle === caption.language
|
_enabled: true
|
||||||
}, false)
|
}
|
||||||
|
|
||||||
|
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 buildQualityLabel (file: VideoFile) {
|
||||||
private initSmoothProgressBar () {
|
let label = file.resolution.label
|
||||||
const SeekBar = videojsUntyped.getComponent('SeekBar')
|
|
||||||
SeekBar.prototype.getPercent = function getPercent () {
|
if (file.fps && file.fps >= 50) {
|
||||||
// Allows for smooth scrubbing, when player can't keep up.
|
label += file.fps
|
||||||
// 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()
|
return label
|
||||||
if (newTime === this.player_.duration()) {
|
}
|
||||||
newTime = newTime - 0.1
|
|
||||||
}
|
private qualitySwitchCallback (id: number) {
|
||||||
this.player_.currentTime(newTime)
|
if (id === -1) {
|
||||||
this.update()
|
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)
|
videojs.registerPlugin('webtorrent', WebTorrentPlugin)
|
||||||
export { PeerTubePlugin }
|
export { WebTorrentPlugin }
|
|
@ -17,17 +17,13 @@ import 'core-js/es6/set'
|
||||||
// For google bot that uses Chrome 41 and does not understand fetch
|
// For google bot that uses Chrome 41 and does not understand fetch
|
||||||
import 'whatwg-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 * as Channel from 'jschannel'
|
||||||
|
|
||||||
import { peertubeTranslate, ResultList, VideoDetails } from '../../../../shared'
|
import { peertubeTranslate, ResultList, VideoDetails } from '../../../../shared'
|
||||||
import { addContextMenu, getServerTranslations, getVideojsOptions, loadLocaleInVideoJS } from '../../assets/player/peertube-player'
|
|
||||||
import { PeerTubeResolution } from '../player/definitions'
|
import { PeerTubeResolution } from '../player/definitions'
|
||||||
import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
|
import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
|
||||||
import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model'
|
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
|
* Embed API exposes control of the embed player to the outside world via
|
||||||
|
@ -73,16 +69,16 @@ class PeerTubeEmbedApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
private setResolution (resolutionId: number) {
|
private setResolution (resolutionId: number) {
|
||||||
if (resolutionId === -1 && this.embed.player.peertube().isAutoResolutionForbidden()) return
|
if (resolutionId === -1 && this.embed.player.webtorrent().isAutoResolutionForbidden()) return
|
||||||
|
|
||||||
// Auto resolution
|
// Auto resolution
|
||||||
if (resolutionId === -1) {
|
if (resolutionId === -1) {
|
||||||
this.embed.player.peertube().enableAutoResolution()
|
this.embed.player.webtorrent().enableAutoResolution()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.embed.player.peertube().disableAutoResolution()
|
this.embed.player.webtorrent().disableAutoResolution()
|
||||||
this.embed.player.peertube().updateResolution(resolutionId)
|
this.embed.player.webtorrent().updateResolution(resolutionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -122,15 +118,17 @@ class PeerTubeEmbedApi {
|
||||||
|
|
||||||
// PeerTube specific capabilities
|
// PeerTube specific capabilities
|
||||||
|
|
||||||
this.embed.player.peertube().on('autoResolutionUpdate', () => this.loadResolutions())
|
if (this.embed.player.webtorrent) {
|
||||||
this.embed.player.peertube().on('videoFileUpdate', () => this.loadResolutions())
|
this.embed.player.webtorrent().on('autoResolutionUpdate', () => this.loadWebTorrentResolutions())
|
||||||
|
this.embed.player.webtorrent().on('videoFileUpdate', () => this.loadWebTorrentResolutions())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadResolutions () {
|
private loadWebTorrentResolutions () {
|
||||||
let resolutions = []
|
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
|
let label = videoFile.resolution.label
|
||||||
if (videoFile.fps && videoFile.fps >= 50) {
|
if (videoFile.fps && videoFile.fps >= 50) {
|
||||||
label += videoFile.fps
|
label += videoFile.fps
|
||||||
|
@ -266,9 +264,8 @@ class PeerTubeEmbed {
|
||||||
const urlParts = window.location.pathname.split('/')
|
const urlParts = window.location.pathname.split('/')
|
||||||
const videoId = urlParts[ urlParts.length - 1 ]
|
const videoId = urlParts[ urlParts.length - 1 ]
|
||||||
|
|
||||||
const [ , serverTranslations, videoResponse, captionsResponse ] = await Promise.all([
|
const [ serverTranslations, videoResponse, captionsResponse ] = await Promise.all([
|
||||||
loadLocaleInVideoJS(window.location.origin, vjs, navigator.language),
|
PeertubePlayerManager.getServerTranslations(window.location.origin, navigator.language),
|
||||||
getServerTranslations(window.location.origin, navigator.language),
|
|
||||||
this.loadVideoInfo(videoId),
|
this.loadVideoInfo(videoId),
|
||||||
this.loadVideoCaptions(videoId)
|
this.loadVideoCaptions(videoId)
|
||||||
])
|
])
|
||||||
|
@ -292,43 +289,56 @@ class PeerTubeEmbed {
|
||||||
|
|
||||||
this.loadParams()
|
this.loadParams()
|
||||||
|
|
||||||
const videojsOptions = getVideojsOptions({
|
const options: PeertubePlayerManagerOptions = {
|
||||||
autoplay: this.autoplay,
|
common: {
|
||||||
controls: this.controls,
|
autoplay: this.autoplay,
|
||||||
muted: this.muted,
|
controls: this.controls,
|
||||||
loop: this.loop,
|
muted: this.muted,
|
||||||
startTime: this.startTime,
|
loop: this.loop,
|
||||||
subtitle: this.subtitle,
|
captions: videoCaptions.length !== 0,
|
||||||
|
startTime: this.startTime,
|
||||||
|
subtitle: this.subtitle,
|
||||||
|
|
||||||
videoCaptions,
|
videoCaptions,
|
||||||
inactivityTimeout: 1500,
|
inactivityTimeout: 1500,
|
||||||
videoViewUrl: this.getVideoUrl(videoId) + '/views',
|
videoViewUrl: this.getVideoUrl(videoId) + '/views',
|
||||||
playerElement: this.videoElement,
|
playerElement: this.videoElement,
|
||||||
videoFiles: videoInfo.files,
|
videoDuration: videoInfo.duration,
|
||||||
videoDuration: videoInfo.duration,
|
enableHotkeys: true,
|
||||||
enableHotkeys: true,
|
peertubeLink: true,
|
||||||
peertubeLink: true,
|
poster: window.location.origin + videoInfo.previewPath,
|
||||||
poster: window.location.origin + videoInfo.previewPath,
|
theaterMode: false,
|
||||||
theaterMode: false
|
|
||||||
})
|
|
||||||
|
|
||||||
this.playerOptions = videojsOptions
|
serverUrl: window.location.origin,
|
||||||
this.player = vjs(this.videoContainerId, videojsOptions, () => {
|
language: navigator.language,
|
||||||
this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations))
|
embedUrl: window.location.origin + videoInfo.embedPath
|
||||||
|
},
|
||||||
|
|
||||||
window[ 'videojsPlayer' ] = this.player
|
webtorrent: {
|
||||||
|
videoFiles: videoInfo.files
|
||||||
if (this.controls) {
|
|
||||||
this.player.dock({
|
|
||||||
title: videoInfo.name,
|
|
||||||
description: this.player.localize('Uses P2P, others may know your IP is downloading this video.')
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addContextMenu(this.player, window.location.origin + videoInfo.embedPath)
|
// 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.initializeApi()
|
this.player = await PeertubePlayerManager.initialize('webtorrent', options)
|
||||||
})
|
|
||||||
|
this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations))
|
||||||
|
|
||||||
|
window[ 'videojsPlayer' ] = this.player
|
||||||
|
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleError (err: Error, translations?: { [ id: string ]: string }) {
|
private handleError (err: Error, translations?: { [ id: string ]: string }) {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "../out-tsc/app",
|
"outDir": "../out-tsc/app",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"module": "es2015",
|
"module": "esnext",
|
||||||
"types": [],
|
"types": [],
|
||||||
"lib": [
|
"lib": [
|
||||||
"es2017",
|
"es2017",
|
||||||
|
|
|
@ -394,6 +394,11 @@
|
||||||
semver "5.5.1"
|
semver "5.5.1"
|
||||||
semver-intersect "1.4.0"
|
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@*":
|
"@types/bittorrent-protocol@*":
|
||||||
version "2.2.2"
|
version "2.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/bittorrent-protocol/-/bittorrent-protocol-2.2.2.tgz#169e9633e1bd18e6b830d11cf42e611b1972cb83"
|
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"
|
unordered-array-remove "^1.0.2"
|
||||||
xtend "^4.0.0"
|
xtend "^4.0.0"
|
||||||
|
|
||||||
bittorrent-tracker@^9.0.0:
|
bittorrent-tracker@^9.0.0, bittorrent-tracker@^9.10.1:
|
||||||
version "9.10.1"
|
version "9.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.10.1.tgz#5de14aac012a287af394d3cc9eda1ec6cc956f11"
|
resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.10.1.tgz#5de14aac012a287af394d3cc9eda1ec6cc956f11"
|
||||||
integrity sha512-n5zTL/g6Wt0rb2EnkiyiaGYhth7I/N0/xMqGUpvGX/7g1scDGBVPhJnXR8lfp3/OMj681fv40o4q/otECMtZSA==
|
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"
|
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
|
||||||
integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=
|
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:
|
eventsource@^1.0.7:
|
||||||
version "1.0.7"
|
version "1.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0"
|
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"
|
resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537"
|
||||||
integrity sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA==
|
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"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/get-browser-rtc/-/get-browser-rtc-1.0.2.tgz#bbcd40c8451a7ed4ef5c373b8169a409dd1d11d9"
|
resolved "https://registry.yarnpkg.com/get-browser-rtc/-/get-browser-rtc-1.0.2.tgz#bbcd40c8451a7ed4ef5c373b8169a409dd1d11d9"
|
||||||
integrity sha1-u81AyEUaftTvXDc7gWmkCd0dEdk=
|
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"
|
resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.2.0.tgz#c8e0785fd17f741f4408b49466889274a9e36447"
|
||||||
integrity sha512-LVHw0U6IPJjwk9i9f7Xe26NqaUHTNlIt4SSWoEfYFROeVKHN6MIjOhbRheI3dg8Jbq5WCuMFQ0QU3EgZpmzFPg==
|
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:
|
magic-string@^0.25.0:
|
||||||
version "0.25.1"
|
version "0.25.1"
|
||||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.1.tgz#b1c248b399cd7485da0fe7385c2fc7011843266e"
|
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"
|
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1"
|
||||||
integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==
|
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:
|
package-json-versionify@^1.0.2:
|
||||||
version "1.0.4"
|
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/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"
|
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
|
||||||
integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
|
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"
|
version "2.4.11"
|
||||||
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
|
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
|
||||||
integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
|
integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
|
||||||
|
@ -10090,6 +10127,14 @@ videojs-contextmenu-ui@^5.0.0:
|
||||||
global "^4.3.2"
|
global "^4.3.2"
|
||||||
video.js "^6 || ^7"
|
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:
|
videojs-dock@^2.0.2:
|
||||||
version "2.1.4"
|
version "2.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/videojs-dock/-/videojs-dock-2.1.4.tgz#0ebd198b5d48990e3523fdc87dbfdb9fe96f804c"
|
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
|
if [ ! -f "./client/dist/en_US/index.html" ]; then
|
||||||
echo "client/dist/en_US/index.html does not exist, compile client files..."
|
echo "client/dist/en_US/index.html does not exist, compile client files..."
|
||||||
npm run build:client
|
npm run build:client -- --light
|
||||||
fi
|
fi
|
||||||
|
|
||||||
npm run watch:server
|
npm run watch:server
|
||||||
|
|
|
@ -16,7 +16,7 @@ const baseDirectives = Object.assign({},
|
||||||
baseUri: ["'self'"],
|
baseUri: ["'self'"],
|
||||||
manifestSrc: ["'self'"],
|
manifestSrc: ["'self'"],
|
||||||
frameSrc: ["'self'"], // instead of deprecated child-src / self because of test-embed
|
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.SERVICES['CSP-LOGGER'] ? { reportUri: CONFIG.SERVICES['CSP-LOGGER'] } : {},
|
||||||
CONFIG.WEBSERVER.SCHEME === 'https' ? { upgradeInsecureRequests: true } : {}
|
CONFIG.WEBSERVER.SCHEME === 'https' ? { upgradeInsecureRequests: true } : {}
|
||||||
|
|
Loading…
Reference in New Issue