mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2024-12-01 04:49:16 -06:00
add stats videojs plugin
This commit is contained in:
parent
0979075453
commit
ff563914bb
1
client/src/assets/player/images/info.svg
Normal file
1
client/src/assets/player/images/info.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="feather feather-info"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
After Width: | Height: | Size: 347 B |
@ -45,6 +45,7 @@ function saveTheaterInStore (enabled: boolean) {
|
||||
}
|
||||
|
||||
function saveAverageBandwidth (value: number) {
|
||||
/** used to choose the most fitting resolution */
|
||||
return setLocalStorage('average-bandwidth', value.toString())
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,8 @@ import 'videojs-contextmenu-pt'
|
||||
import 'videojs-contrib-quality-levels'
|
||||
import './upnext/end-card'
|
||||
import './upnext/upnext-plugin'
|
||||
import './stats/stats-card'
|
||||
import './stats/stats-plugin'
|
||||
import './bezels/bezels-plugin'
|
||||
import './peertube-plugin'
|
||||
import './videojs-components/next-previous-video-button'
|
||||
@ -170,6 +172,11 @@ export class PeertubePlayerManager {
|
||||
self.addContextMenu(mode, player, options.common.embedUrl, options.common.embedTitle)
|
||||
|
||||
player.bezels()
|
||||
player.stats({
|
||||
videoUUID: options.common.videoUUID,
|
||||
videoIsLive: options.common.isLive,
|
||||
mode
|
||||
})
|
||||
|
||||
return res(player)
|
||||
})
|
||||
@ -538,6 +545,14 @@ export class PeertubePlayerManager {
|
||||
})
|
||||
}
|
||||
|
||||
items.push({
|
||||
icon: 'info',
|
||||
label: player.localize('Stats for nerds'),
|
||||
listener: () => {
|
||||
player.stats().show()
|
||||
}
|
||||
})
|
||||
|
||||
return items.map(i => ({
|
||||
...i,
|
||||
label: `<span class="vjs-icon-${i.icon || 'link-2'}"></span>` + i.label
|
||||
|
@ -7,6 +7,7 @@ import { PlayerMode } from './peertube-player-manager'
|
||||
import { PeerTubePlugin } from './peertube-plugin'
|
||||
import { PlaylistPlugin } from './playlist/playlist-plugin'
|
||||
import { EndCardOptions } from './upnext/end-card'
|
||||
import { StatsCardOptions } from './stats/stats-card'
|
||||
import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
|
||||
|
||||
declare module 'video.js' {
|
||||
@ -36,6 +37,8 @@ declare module 'video.js' {
|
||||
|
||||
bezels (): void
|
||||
|
||||
stats (options?: Partial<StatsCardOptions>): any
|
||||
|
||||
qualityLevels (): QualityLevels
|
||||
|
||||
textTracks (): TextTrackList & {
|
||||
|
184
client/src/assets/player/stats/stats-card.ts
Normal file
184
client/src/assets/player/stats/stats-card.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import videojs from 'video.js'
|
||||
import { PlayerNetworkInfo } from '../peertube-videojs-typings'
|
||||
import { getAverageBandwidthInStore } from '../peertube-player-local-storage'
|
||||
import { bytes } from '../utils'
|
||||
|
||||
interface StatsCardOptions extends videojs.ComponentOptions {
|
||||
videoUUID?: string,
|
||||
videoIsLive?: boolean,
|
||||
mode?: 'webtorrent' | 'p2p-media-loader'
|
||||
}
|
||||
|
||||
function getListTemplate (
|
||||
options: StatsCardOptions,
|
||||
player: videojs.Player,
|
||||
args: {
|
||||
playerNetworkInfo?: any
|
||||
videoFile?: any
|
||||
progress?: number
|
||||
}) {
|
||||
const { playerNetworkInfo, videoFile, progress } = args
|
||||
|
||||
const videoQuality: VideoPlaybackQuality = player.getVideoPlaybackQuality()
|
||||
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
|
||||
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
|
||||
const pr = (window.devicePixelRatio || 1).toFixed(2)
|
||||
const colorspace = videoFile?.metadata?.streams[0]['color_space'] !== "unknown"
|
||||
? videoFile?.metadata?.streams[0]['color_space']
|
||||
: undefined
|
||||
|
||||
return `
|
||||
<div>
|
||||
<div>${player.localize('Video UUID')}</div>
|
||||
<span>${options.videoUUID || ''}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div>Viewport / ${player.localize('Frames')}</div>
|
||||
<span>${vw}x${vh}*${pr} / ${videoQuality.droppedVideoFrames} dropped of ${videoQuality.totalVideoFrames}</span>
|
||||
</div>
|
||||
<div${videoFile !== undefined ? '' : ' style="display: none;"'}>
|
||||
<div>${player.localize('Resolution')}</div>
|
||||
<span>${videoFile?.resolution.label + videoFile?.fps}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div>${player.localize('Volume')}</div>
|
||||
<span>${~~(player.volume() * 100)}%${player.muted() ? ' (muted)' : ''}</span>
|
||||
</div>
|
||||
<div${videoFile !== undefined ? '' : ' style="display: none;"'}>
|
||||
<div>${player.localize('Codecs')}</div>
|
||||
<span>${videoFile?.metadata?.streams[0]['codec_name'] || 'avc1'}</span>
|
||||
</div>
|
||||
<div${videoFile !== undefined ? '' : ' style="display: none;"'}>
|
||||
<div>${player.localize('Color')}</div>
|
||||
<span>${colorspace || 'bt709'}</span>
|
||||
</div>
|
||||
<div${playerNetworkInfo.averageBandwidth !== undefined ? '' : ' style="display: none;"'}>
|
||||
<div>${player.localize('Connection Speed')}</div>
|
||||
<span>${playerNetworkInfo.averageBandwidth}</span>
|
||||
</div>
|
||||
<div${playerNetworkInfo.downloadSpeed !== undefined ? '' : ' style="display: none;"'}>
|
||||
<div>${player.localize('Network Activity')}</div>
|
||||
<span>${playerNetworkInfo.downloadSpeed} ⇓ / ${playerNetworkInfo.uploadSpeed} ⇑</span>
|
||||
</div>
|
||||
<div${playerNetworkInfo.totalDownloaded !== undefined ? '' : ' style="display: none;"'}>
|
||||
<div>${player.localize('Total Transfered')}</div>
|
||||
<span>${playerNetworkInfo.totalDownloaded} ⇓ / ${playerNetworkInfo.totalUploaded} ⇑</span>
|
||||
</div>
|
||||
<div${playerNetworkInfo.downloadedFromServer ? '' : ' style="display: none;"'}>
|
||||
<div>${player.localize('Download Breakdown')}</div>
|
||||
<span>${playerNetworkInfo.downloadedFromServer} from server · ${playerNetworkInfo.downloadedFromPeers} from peers</span>
|
||||
</div>
|
||||
<div${progress !== undefined && videoFile !== undefined ? '' : ' style="display: none;"'}>
|
||||
<div>${player.localize('Buffer Health')}</div>
|
||||
<span>${(progress * 100).toFixed(1)}% (${(progress * videoFile?.metadata?.format.duration).toFixed(1)}s)</span>
|
||||
</div>
|
||||
<div style="display: none;"> <!-- TODO: implement live latency measure -->
|
||||
<div>${player.localize('Live Latency')}</div>
|
||||
<span></span>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function getMainTemplate () {
|
||||
return `
|
||||
<button class="vjs-stats-close" tabindex=0 aria-label="Close stats" title="Close stats">[x]</button>
|
||||
<div class="vjs-stats-list"></div>
|
||||
`
|
||||
}
|
||||
|
||||
const Component = videojs.getComponent('Component')
|
||||
class StatsCard extends Component {
|
||||
options_: StatsCardOptions
|
||||
container: HTMLDivElement
|
||||
list: HTMLDivElement
|
||||
closeButton: HTMLElement
|
||||
update: any
|
||||
source: any
|
||||
|
||||
interval = 300
|
||||
playerNetworkInfo: any = {}
|
||||
statsForNerdsEvents = new videojs.EventTarget()
|
||||
|
||||
constructor (player: videojs.Player, options: StatsCardOptions) {
|
||||
super(player, options)
|
||||
}
|
||||
|
||||
createEl () {
|
||||
const container = super.createEl('div', {
|
||||
className: 'vjs-stats-content',
|
||||
innerHTML: getMainTemplate()
|
||||
}) as HTMLDivElement
|
||||
this.container = container
|
||||
this.container.style.display = 'none'
|
||||
|
||||
this.closeButton = this.container.getElementsByClassName('vjs-stats-close')[0] as HTMLElement
|
||||
this.closeButton.onclick = () => this.hide()
|
||||
|
||||
this.list = this.container.getElementsByClassName('vjs-stats-list')[0] as HTMLDivElement
|
||||
|
||||
console.log(this.player_.qualityLevels())
|
||||
|
||||
this.player_.on('p2pInfo', (event: any, data: PlayerNetworkInfo) => {
|
||||
if (!data) return // HTTP fallback
|
||||
|
||||
this.source = data.source
|
||||
|
||||
const p2pStats = data.p2p
|
||||
const httpStats = data.http
|
||||
|
||||
this.playerNetworkInfo.downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed).join(' ')
|
||||
this.playerNetworkInfo.uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed).join(' ')
|
||||
this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ')
|
||||
this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded).join(' ')
|
||||
this.playerNetworkInfo.numPeers = p2pStats.numPeers
|
||||
this.playerNetworkInfo.averageBandwidth = bytes(getAverageBandwidthInStore() || p2pStats.downloaded + httpStats.downloaded).join(' ')
|
||||
|
||||
if (data.source === 'p2p-media-loader') {
|
||||
this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ')
|
||||
this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats.downloaded).join(' ')
|
||||
}
|
||||
})
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
toggle () {
|
||||
this.update
|
||||
? this.hide()
|
||||
: this.show()
|
||||
}
|
||||
|
||||
show (options?: StatsCardOptions) {
|
||||
if (options) this.options_ = options
|
||||
|
||||
let metadata = {}
|
||||
|
||||
this.container.style.display = 'block'
|
||||
this.update = setInterval(async () => {
|
||||
try {
|
||||
if (this.source === 'webtorrent') {
|
||||
const progress = this.player_.webtorrent().getTorrent()?.progress
|
||||
const videoFile = this.player_.webtorrent().getCurrentVideoFile()
|
||||
videoFile.metadata = metadata[videoFile.fileUrl] = videoFile.metadata || metadata[videoFile.fileUrl] || videoFile.metadataUrl && await fetch(videoFile.metadataUrl).then(res => res.json())
|
||||
this.list.innerHTML = getListTemplate(this.options_, this.player_, { playerNetworkInfo: this.playerNetworkInfo, videoFile, progress })
|
||||
} else {
|
||||
this.list.innerHTML = getListTemplate(this.options_, this.player_, { playerNetworkInfo: this.playerNetworkInfo })
|
||||
}
|
||||
} catch (e) {
|
||||
clearInterval(this.update)
|
||||
}
|
||||
}, this.interval)
|
||||
}
|
||||
|
||||
hide () {
|
||||
clearInterval(this.update)
|
||||
this.container.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
videojs.registerComponent('StatsCard', StatsCard)
|
||||
|
||||
export {
|
||||
StatsCard,
|
||||
StatsCardOptions
|
||||
}
|
31
client/src/assets/player/stats/stats-plugin.ts
Normal file
31
client/src/assets/player/stats/stats-plugin.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import videojs from 'video.js'
|
||||
import { StatsCard, StatsCardOptions } from './stats-card'
|
||||
|
||||
const Plugin = videojs.getPlugin('plugin')
|
||||
|
||||
class StatsForNerdsPlugin extends Plugin {
|
||||
private statsCard: StatsCard
|
||||
|
||||
constructor (player: videojs.Player, options: Partial<StatsCardOptions> = {}) {
|
||||
const settings = {
|
||||
...options
|
||||
}
|
||||
|
||||
super(player)
|
||||
|
||||
this.player.ready(() => {
|
||||
player.addClass('vjs-stats-for-nerds')
|
||||
})
|
||||
|
||||
this.statsCard = new StatsCard(player, options)
|
||||
|
||||
player.addChild(this.statsCard, settings)
|
||||
}
|
||||
|
||||
show (options?: StatsCardOptions) {
|
||||
this.statsCard.show(options)
|
||||
}
|
||||
}
|
||||
|
||||
videojs.registerPlugin('stats', StatsForNerdsPlugin)
|
||||
export { StatsForNerdsPlugin }
|
@ -8,7 +8,7 @@ $context-menu-width: 350px;
|
||||
|
||||
.video-js .vjs-contextmenu-ui-menu {
|
||||
position: absolute;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
background-color: $primary-background-color;
|
||||
padding: 8px 0;
|
||||
border-radius: 4px;
|
||||
width: $context-menu-width;
|
||||
@ -42,7 +42,7 @@ $context-menu-width: 350px;
|
||||
mask-size: cover;
|
||||
margin-right: 0.8rem !important;
|
||||
|
||||
$icons: 'link-2', 'repeat', 'code', 'tick-white';
|
||||
$icons: 'link-2', 'repeat', 'code', 'tick-white', 'info';
|
||||
|
||||
@each $icon in $icons {
|
||||
&[class$="-#{$icon}"] {
|
||||
|
@ -6,3 +6,4 @@
|
||||
@import './upnext';
|
||||
@import './bezels.scss';
|
||||
@import './playlist.scss';
|
||||
@import './stats.scss';
|
||||
|
42
client/src/sass/player/stats.scss
Normal file
42
client/src/sass/player/stats.scss
Normal file
@ -0,0 +1,42 @@
|
||||
@import './_player-variables';
|
||||
|
||||
$stats-width: 420px;
|
||||
$contextmenu-background-color: rgba(0, 0, 0, 0.6);
|
||||
|
||||
.video-js {
|
||||
|
||||
.vjs-stats-content {
|
||||
position: absolute;
|
||||
background-color: $contextmenu-background-color;
|
||||
padding: 5px 0;
|
||||
border-radius: 4px;
|
||||
width: $stats-width;
|
||||
min-width: 27em;
|
||||
max-width: calc(100vw - 20px);
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
z-index: 64;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
|
||||
@include transition(opacity 0.1s);
|
||||
}
|
||||
|
||||
.vjs-stats-close {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
top: 3px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.vjs-stats-list > div > div {
|
||||
display: inline-block;
|
||||
font-weight: 600;
|
||||
padding: 0 .5em;
|
||||
text-align: right;
|
||||
width: 11.5em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user