"@ngx-translate/i18n-polyfill": "^1.0.0",
"@streamroot/videojs-hlsjs-plugin": "^1.0.7",
"@types/core-js": "^2.5.0",
+ "@types/hls.js": "^0.12.0",
"@types/jasmine": "^2.8.7",
"@types/jasminewd2": "^2.0.3",
"@types/jest": "^23.3.1",
"extract-text-webpack-plugin": "4.0.0-beta.0",
"file-loader": "^2.0.0",
"focus-visible": "^4.1.5",
+ "hls.js": "^0.12.2",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"https-browserify": "^1.0.0",
// 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'
+import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo, 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'
+import { Engine, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs'
+import * as Hls from 'hls.js'
+import { Events } from 'p2p-media-loader-core'
const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
class P2pMediaLoaderPlugin extends Plugin {
+ private readonly CONSTANTS = {
+ INFO_SCHEDULER: 1000 // Don't change this
+ }
+
+ private hlsjs: Hls
+ private p2pEngine: Engine
+ private statsP2PBytes = {
+ pendingDownload: [] as number[],
+ pendingUpload: [] as number[],
+ numPeers: 0,
+ totalDownload: 0,
+ totalUpload: 0
+ }
+
+ private networkInfoInterval: any
+
constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) {
super(player, options)
- initVideoJsContribHlsJsPlayer(player)
+ videojs.Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: Hls) => {
+ this.hlsjs = hlsjs
- console.log(options)
+ this.initialize()
+ })
+
+ initVideoJsContribHlsJsPlayer(player)
player.src({
type: options.type,
})
}
+ dispose () {
+ clearInterval(this.networkInfoInterval)
+ }
+
+ private initialize () {
+ this.p2pEngine = this.player.tech_.options_.hlsjsConfig.loader.getEngine()
+
+ this.hlsjs.on(Hls.Events.LEVEL_SWITCHING, (_, data: Hls.levelSwitchingData) => {
+ this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height })
+ })
+
+ this.runStats()
+ }
+
+ private runStats () {
+ this.p2pEngine.on(Events.PieceBytesDownloaded, (method: string, size: number) => {
+ if (method === 'p2p') {
+ this.statsP2PBytes.pendingDownload.push(size)
+ this.statsP2PBytes.totalDownload += size
+ }
+ })
+
+ this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, size: number) => {
+ if (method === 'p2p') {
+ this.statsP2PBytes.pendingUpload.push(size)
+ this.statsP2PBytes.totalUpload += size
+ }
+ })
+
+ this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++)
+ this.p2pEngine.on(Events.PeerClose, () => this.statsP2PBytes.numPeers--)
+
+ this.networkInfoInterval = setInterval(() => {
+ let downloadSpeed = this.statsP2PBytes.pendingDownload.reduce((a: number, b: number) => a + b, 0)
+ let uploadSpeed = this.statsP2PBytes.pendingUpload.reduce((a: number, b: number) => a + b, 0)
+
+ this.statsP2PBytes.pendingDownload = []
+ this.statsP2PBytes.pendingUpload = []
+
+ return this.player.trigger('p2pInfo', {
+ p2p: {
+ downloadSpeed,
+ uploadSpeed,
+ numPeers: this.statsP2PBytes.numPeers,
+ downloaded: this.statsP2PBytes.totalDownload,
+ uploaded: this.statsP2PBytes.totalUpload
+ }
+ } as PlayerNetworkInfo)
+ }, this.CONSTANTS.INFO_SCHEDULER)
+ }
}
videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin)
// 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'
+export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
-type WebtorrentOptions = {
+export type WebtorrentOptions = {
videoFiles: VideoFile[]
}
-type P2PMediaLoaderOptions = {
+export type P2PMediaLoaderOptions = {
playlistUrl: string
}
-type CommonOptions = {
+export type CommonOptions = {
playerElement: HTMLVideoElement
autoplay: boolean
const commonOptions = options.common
const webtorrentOptions = options.webtorrent
const p2pMediaLoaderOptions = options.p2pMediaLoader
+ let html5 = {}
const plugins: VideoJSPluginOptions = {
peertube: {
}
Object.assign(plugins, { p2pMediaLoader, streamrootHls })
+ html5 = streamrootHls.html5
}
if (webtorrentOptions) {
}
const videojsOptions = {
+ html5,
+
// We don't use text track settings for now
textTrackSettings: false,
controls: commonOptions.controls !== undefined ? commonOptions.controls : true,
// @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 {
+ PeerTubePluginOptions,
+ ResolutionUpdateData,
+ UserWatching,
+ VideoJSCaption,
+ VideoJSComponentInterface,
+ videojsUntyped
+} from './peertube-videojs-typings'
import { isMobile, timeToInt } from './utils'
import {
getStoredLastSubtitle,
private videoViewInterval: any
private userWatchingVideoInterval: any
private qualityObservationTimer: any
+ private lastResolutionChange: ResolutionUpdateData
constructor (player: videojs.Player, options: PeerTubePluginOptions) {
super(player, options)
this.player.ready(() => {
const playerOptions = this.player.options_
+ if (this.player.webtorrent) {
+ this.player.webtorrent().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
+ this.player.webtorrent().on('autoResolutionChange', (_: any, d: any) => this.trigger('autoResolutionChange', d))
+ }
+
+ if (this.player.p2pMediaLoader) {
+ this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
+ }
+
+ this.player.tech_.on('loadedqualitydata', () => {
+ setTimeout(() => {
+ // Replay a resolution change, now we loaded all quality data
+ if (this.lastResolutionChange) this.handleResolutionChange(this.lastResolutionChange)
+ }, 0)
+ })
+
const volume = getStoredVolume()
if (volume !== undefined) this.player.volume(volume)
return fetch(url, { method: 'PUT', body, headers })
}
+ private handleResolutionChange (data: ResolutionUpdateData) {
+ this.lastResolutionChange = data
+
+ const qualityLevels = this.player.qualityLevels()
+
+ for (let i = 0; i < qualityLevels.length; i++) {
+ if (qualityLevels[i].height === data.resolutionId) {
+ data.id = qualityLevels[i].id
+ break
+ }
+ }
+
+ this.trigger('resolutionChange', data)
+ }
+
private alterInactivity () {
let saveInactivityTimeout: number
type ResolutionUpdateData = {
auto: boolean,
resolutionId: number
+ id?: number
}
type AutoResolutionUpdateData = {
possible: boolean
}
+type PlayerNetworkInfo = {
+ p2p: {
+ downloadSpeed: number
+ uploadSpeed: number
+ downloaded: number
+ uploaded: number
+ numPeers: number
+ }
+}
+
export {
+ PlayerNetworkInfo,
ResolutionUpdateData,
AutoResolutionUpdateData,
VideoJSComponentInterface,
-import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
+import { PlayerNetworkInfo, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
import { bytes } from '../utils'
const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
subDivHttp.appendChild(subDivHttpText)
div.appendChild(subDivHttp)
- this.player_.on('p2pInfo', (event: any, data: any) => {
+ this.player_.on('p2pInfo', (event: any, data: PlayerNetworkInfo) => {
// We are in HTTP fallback
if (!data) {
subDivHttp.className = 'vjs-peertube-displayed'
return
}
- const downloadSpeed = bytes(data.downloadSpeed)
- const uploadSpeed = bytes(data.uploadSpeed)
- const totalDownloaded = bytes(data.downloaded)
- const totalUploaded = bytes(data.uploaded)
- const numPeers = data.numPeers
+ const p2pStats = data.p2p
+
+ const downloadSpeed = bytes(p2pStats.downloadSpeed)
+ const uploadSpeed = bytes(p2pStats.uploadSpeed)
+ const totalDownloaded = bytes(p2pStats.downloaded)
+ const totalUploaded = bytes(p2pStats.uploaded)
+ const numPeers = p2pStats.numPeers
subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' +
this.player_.localize('Total uploaded: ' + totalUploaded.join(' '))
super(player, options)
this.player = player
- player.on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data))
+ player.tech_.on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data))
- if (player.webtorrent) {
- player.webtorrent().on('videoFileUpdate', () => setTimeout(() => this.trigger('updateLabel'), 0))
- }
+ player.peertube().on('resolutionChange', () => setTimeout(() => this.trigger('updateLabel'), 0))
}
createEl () {
return 'vjs-resolution-control ' + super.buildWrapperCSSClass()
}
+ private addClickListener (component: any) {
+ component.on('click', () => {
+ let children = this.menu.children()
+
+ for (const child of children) {
+ if (component !== child) {
+ child.selected(false)
+ }
+ }
+ })
+ }
+
private buildQualities (data: LoadedQualityData) {
// The automatic resolution item will need other labels
const labels: { [ id: number ]: string } = {}
+ data.qualityData.video.sort((a, b) => {
+ if (a.id > b.id) return -1
+ if (a.id === b.id) return 0
+ return 1
+ })
+
for (const d of data.qualityData.video) {
+ // Skip auto resolution, we'll add it ourselves
+ if (d.id === -1) continue
+
this.menu.addChild(new ResolutionMenuItem(
this.player_,
{
selected: true // By default, in auto mode
}
))
+
+ for (const m of this.menu.children()) {
+ this.addClickListener(m)
+ }
+
+ this.trigger('menuChanged')
}
}
ResolutionMenuButton.prototype.controlText_ = 'Quality'
this.id = options.id
this.callback = options.callback
- if (player.webtorrent) {
- player.webtorrent().on('videoFileUpdate', (_: any, data: ResolutionUpdateData) => this.updateSelection(data))
+ player.peertube().on('resolutionChange', (_: 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))
- }
+ // We only want to disable the "Auto" item
+ if (this.id === -1) {
+ player.peertube().on('autoResolutionChange', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data))
}
-
- // TODO: update on HLS change
}
handleClick (event: any) {
super.handleClick(event)
- this.callback(this.id)
+ this.callback(this.id, 'video')
}
updateSelection (data: ResolutionUpdateData) {
if (this.id === -1) {
- this.currentResolutionLabel = this.labels[data.resolutionId]
+ this.currentResolutionLabel = this.labels[data.id]
}
// Automatic resolution only
return
}
- this.selected(this.id === data.resolutionId)
+ this.selected(this.id === data.id)
}
updateAutoResolution (data: AutoResolutionUpdateData) {
this.subMenu.on('updateLabel', () => {
this.update()
})
+ this.subMenu.on('menuChanged', () => {
+ this.bindClickEvents()
+ this.setSize()
+ this.update()
+ })
this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_)
this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_)
this.update()
this.createBackButton()
- this.getSize()
+ this.setSize()
this.bindClickEvents()
// prefixed event listeners for CSS TransitionEnd
// save size of submenus on first init
// if number of submenu items change dynamically more logic will be needed
- getSize () {
+ setSize () {
this.dialog.removeClass('vjs-hidden')
+ videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden')
this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_)
this.setMargin()
this.dialog.addClass('vjs-hidden')
import * as WebTorrent from 'webtorrent'
import { VideoFile } from '../../../../shared/models/videos/video.model'
import { renderVideo } from './webtorrent/video-renderer'
-import { LoadedQualityData, VideoJSComponentInterface, WebtorrentPluginOptions } from './peertube-videojs-typings'
+import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from './peertube-videojs-typings'
import { videoFileMaxByResolution, videoFileMinByResolution } from './utils'
import { PeertubeChunkStore } from './webtorrent/peertube-chunk-store'
import {
})
this.changeQuality()
- this.trigger('videoFileUpdate', { auto: this.autoResolution, resolutionId: this.currentVideoFile.resolution.id })
+ this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.currentVideoFile.resolution.id })
}
updateResolution (resolutionId: number, delay = 0) {
enableAutoResolution () {
this.autoResolution = true
- this.trigger('videoFileUpdate', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() })
+ this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() })
}
disableAutoResolution (forbid = false) {
if (forbid === true) this.autoResolutionPossible = false
this.autoResolution = false
- this.trigger('autoResolutionUpdate', { possible: this.autoResolutionPossible })
- this.trigger('videoFileUpdate', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() })
+ this.trigger('autoResolutionChange', { possible: this.autoResolutionPossible })
+ this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() })
}
getTorrent () {
if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed)
return this.player.trigger('p2pInfo', {
- downloadSpeed: this.torrent.downloadSpeed,
- numPeers: this.torrent.numPeers,
- uploadSpeed: this.torrent.uploadSpeed,
- downloaded: this.torrent.downloaded,
- uploaded: this.torrent.uploaded
- })
+ p2p: {
+ downloadSpeed: this.torrent.downloadSpeed,
+ numPeers: this.torrent.numPeers,
+ uploadSpeed: this.torrent.uploadSpeed,
+ downloaded: this.torrent.downloaded,
+ uploaded: this.torrent.uploaded
+ }
+ } as PlayerNetworkInfo)
}, this.CONSTANTS.INFO_SCHEDULER)
}
video: qualityLevelsPayload
}
}
- this.player.trigger('loadedqualitydata', payload)
+ this.player.tech_.trigger('loadedqualitydata', payload)
}
private buildQualityLabel (file: VideoFile) {
import { PeerTubeResolution } from '../player/definitions'
import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model'
-import { PeertubePlayerManager, PeertubePlayerManagerOptions } from '../../assets/player/peertube-player-manager'
+import { PeertubePlayerManager, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager'
/**
* Embed API exposes control of the embed player to the outside world via
subtitle: string
enableApi = false
startTime: number | string = 0
+ mode: PlayerMode
scope = 'peertube'
static async main () {
this.scope = this.getParamString(params, 'scope', this.scope)
this.subtitle = this.getParamString(params, 'subtitle')
this.startTime = this.getParamString(params, 'start')
+
+ this.mode = this.getParamToggle(params, 'p2p-media-loader') ? 'p2p-media-loader' : 'webtorrent'
} catch (err) {
console.error('Cannot get params from URL.', err)
}
serverUrl: window.location.origin,
language: navigator.language,
embedUrl: window.location.origin + videoInfo.embedPath
- },
-
- webtorrent: {
- videoFiles: videoInfo.files
}
+ }
- // p2pMediaLoader: {
- // // playlistUrl: 'https://akamai-axtest.akamaized.net/routes/lapd-v1-acceptance/www_c4/Manifest.m3u8'
- // // playlistUrl: 'https://d2zihajmogu5jn.cloudfront.net/bipbop-advanced/bipbop_16x9_variant.m3u8'
- // playlistUrl: 'https://cdn.theoplayer.com/video/elephants-dream/playlist.m3u8'
- // }
+ if (this.mode === 'p2p-media-loader') {
+ Object.assign(options, {
+ 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'
+ }
+ })
+ } else {
+ Object.assign(options, {
+ webtorrent: {
+ videoFiles: videoInfo.files
+ }
+ })
}
- this.player = await PeertubePlayerManager.initialize('webtorrent', options)
+ this.player = await PeertubePlayerManager.initialize(this.mode, options)
this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations))
resolved "https://registry.yarnpkg.com/@types/core-js/-/core-js-2.5.0.tgz#35cc282488de6f10af1d92902899a3b8ca3fbc47"
integrity sha512-qjkHL3wF0JMHMqgm/kmL8Pf8rIiqvueEiZ0g6NVTcBX1WN46GWDr+V5z+gsHUeL0n8TfAmXnYmF7ajsxmBp4PQ==
+"@types/hls.js@^0.12.0":
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/@types/hls.js/-/hls.js-0.12.0.tgz#33f73e542201a766fa56792cb81fe9f97d7097ed"
+ integrity sha512-hJ7eJAQVEazAANK4Ay0YbXlZF36SDy9c8kcHTF7//77ylgV6hV/JrlwhVmobsSacr5aZcbw5MbZ2bSHbS36eOQ==
+
"@types/jasmine@*":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.3.1.tgz#b6c4f356013364e98b583647c7b3b6de6fccd2cc"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
-eventemitter3@^3.0.0:
+eventemitter3@3.1.0, eventemitter3@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163"
integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+hls.js@^0.12.2:
+ version "0.12.2"
+ resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-0.12.2.tgz#64a969a78cc25991ed5de19357b1dc3f178ac23b"
+ integrity sha512-lQBSXggw9OzEuaUllUBoSxPcf7neFgnEiDRfCdCNdIPtUeV7vXZ0OeASx6EWtZTBiqSSPigoOX1Y+AR5dA1Feg==
+ dependencies:
+ eventemitter3 "3.1.0"
+ url-toolkit "^2.1.6"
+
hmac-drbg@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
querystringify "^2.0.0"
requires-port "^1.0.0"
-url-toolkit@^2.1.1, url-toolkit@^2.1.3:
+url-toolkit@^2.1.1, url-toolkit@^2.1.3, url-toolkit@^2.1.6:
version "2.1.6"
resolved "https://registry.yarnpkg.com/url-toolkit/-/url-toolkit-2.1.6.tgz#6d03246499e519aad224c44044a4ae20544154f2"
integrity sha512-UaZ2+50am4HwrV2crR/JAf63Q4VvPYphe63WGeoJxeu8gmOm0qxPt+KsukfakPNrX9aymGNEkkaoICwn+OuvBw==