aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/assets/player/stats/stats-card.ts
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/assets/player/stats/stats-card.ts')
-rw-r--r--client/src/assets/player/stats/stats-card.ts307
1 files changed, 197 insertions, 110 deletions
diff --git a/client/src/assets/player/stats/stats-card.ts b/client/src/assets/player/stats/stats-card.ts
index 278899b72..f66766089 100644
--- a/client/src/assets/player/stats/stats-card.ts
+++ b/client/src/assets/player/stats/stats-card.ts
@@ -1,103 +1,42 @@
1import videojs from 'video.js' 1import videojs from 'video.js'
2import { PlayerNetworkInfo } from '../peertube-videojs-typings' 2import { PlayerNetworkInfo as EventPlayerNetworkInfo } from '../peertube-videojs-typings'
3import { getAverageBandwidthInStore } from '../peertube-player-local-storage' 3import { bytes, secondsToTime } from '../utils'
4import { bytes } from '../utils'
5 4
6interface StatsCardOptions extends videojs.ComponentOptions { 5interface StatsCardOptions extends videojs.ComponentOptions {
7 videoUUID?: string, 6 videoUUID: string
8 videoIsLive?: boolean, 7 videoIsLive: boolean
9 mode?: 'webtorrent' | 'p2p-media-loader' 8 mode: 'webtorrent' | 'p2p-media-loader'
10} 9}
11 10
12function getListTemplate ( 11interface PlayerNetworkInfo {
13 options: StatsCardOptions, 12 downloadSpeed?: string
14 player: videojs.Player, 13 uploadSpeed?: string
15 args: { 14 totalDownloaded?: string
16 playerNetworkInfo?: any 15 totalUploaded?: string
17 videoFile?: any 16 numPeers?: number
18 progress?: number 17 averageBandwidth?: string
19 }) {
20 const { playerNetworkInfo, videoFile, progress } = args
21
22 const videoQuality: VideoPlaybackQuality = player.getVideoPlaybackQuality()
23 const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
24 const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
25 const pr = (window.devicePixelRatio || 1).toFixed(2)
26 const colorspace = videoFile?.metadata?.streams[0]['color_space'] !== "unknown"
27 ? videoFile?.metadata?.streams[0]['color_space']
28 : undefined
29
30 return `
31 <div>
32 <div>${player.localize('Video UUID')}</div>
33 <span>${options.videoUUID || ''}</span>
34 </div>
35 <div>
36 <div>Viewport / ${player.localize('Frames')}</div>
37 <span>${vw}x${vh}*${pr} / ${videoQuality.droppedVideoFrames} dropped of ${videoQuality.totalVideoFrames}</span>
38 </div>
39 <div${videoFile !== undefined ? '' : ' style="display: none;"'}>
40 <div>${player.localize('Resolution')}</div>
41 <span>${videoFile?.resolution.label + videoFile?.fps}</span>
42 </div>
43 <div>
44 <div>${player.localize('Volume')}</div>
45 <span>${~~(player.volume() * 100)}%${player.muted() ? ' (muted)' : ''}</span>
46 </div>
47 <div${videoFile !== undefined ? '' : ' style="display: none;"'}>
48 <div>${player.localize('Codecs')}</div>
49 <span>${videoFile?.metadata?.streams[0]['codec_name'] || 'avc1'}</span>
50 </div>
51 <div${videoFile !== undefined ? '' : ' style="display: none;"'}>
52 <div>${player.localize('Color')}</div>
53 <span>${colorspace || 'bt709'}</span>
54 </div>
55 <div${playerNetworkInfo.averageBandwidth !== undefined ? '' : ' style="display: none;"'}>
56 <div>${player.localize('Connection Speed')}</div>
57 <span>${playerNetworkInfo.averageBandwidth}</span>
58 </div>
59 <div${playerNetworkInfo.downloadSpeed !== undefined ? '' : ' style="display: none;"'}>
60 <div>${player.localize('Network Activity')}</div>
61 <span>${playerNetworkInfo.downloadSpeed} &dArr; / ${playerNetworkInfo.uploadSpeed} &uArr;</span>
62 </div>
63 <div${playerNetworkInfo.totalDownloaded !== undefined ? '' : ' style="display: none;"'}>
64 <div>${player.localize('Total Transfered')}</div>
65 <span>${playerNetworkInfo.totalDownloaded} &dArr; / ${playerNetworkInfo.totalUploaded} &uArr;</span>
66 </div>
67 <div${playerNetworkInfo.downloadedFromServer ? '' : ' style="display: none;"'}>
68 <div>${player.localize('Download Breakdown')}</div>
69 <span>${playerNetworkInfo.downloadedFromServer} from server · ${playerNetworkInfo.downloadedFromPeers} from peers</span>
70 </div>
71 <div${progress !== undefined && videoFile !== undefined ? '' : ' style="display: none;"'}>
72 <div>${player.localize('Buffer Health')}</div>
73 <span>${(progress * 100).toFixed(1)}% (${(progress * videoFile?.metadata?.format.duration).toFixed(1)}s)</span>
74 </div>
75 <div style="display: none;"> <!-- TODO: implement live latency measure -->
76 <div>${player.localize('Live Latency')}</div>
77 <span></span>
78 </div>
79 `
80}
81 18
82function getMainTemplate () { 19 downloadedFromServer?: string
83 return ` 20 downloadedFromPeers?: string
84 <button class="vjs-stats-close" tabindex=0 aria-label="Close stats" title="Close stats">[x]</button>
85 <div class="vjs-stats-list"></div>
86 `
87} 21}
88 22
89const Component = videojs.getComponent('Component') 23const Component = videojs.getComponent('Component')
90class StatsCard extends Component { 24class StatsCard extends Component {
91 options_: StatsCardOptions 25 options_: StatsCardOptions
26
92 container: HTMLDivElement 27 container: HTMLDivElement
28
93 list: HTMLDivElement 29 list: HTMLDivElement
94 closeButton: HTMLElement 30 closeButton: HTMLElement
95 update: any
96 source: any
97 31
98 interval = 300 32 updateInterval: any
99 playerNetworkInfo: any = {} 33
100 statsForNerdsEvents = new videojs.EventTarget() 34 mode: 'webtorrent' | 'p2p-media-loader'
35
36 metadataStore: any = {}
37
38 intervalMs = 300
39 playerNetworkInfo: PlayerNetworkInfo = {}
101 40
102 constructor (player: videojs.Player, options: StatsCardOptions) { 41 constructor (player: videojs.Player, options: StatsCardOptions) {
103 super(player, options) 42 super(player, options)
@@ -106,7 +45,7 @@ class StatsCard extends Component {
106 createEl () { 45 createEl () {
107 const container = super.createEl('div', { 46 const container = super.createEl('div', {
108 className: 'vjs-stats-content', 47 className: 'vjs-stats-content',
109 innerHTML: getMainTemplate() 48 innerHTML: this.getMainTemplate()
110 }) as HTMLDivElement 49 }) as HTMLDivElement
111 this.container = container 50 this.container = container
112 this.container.style.display = 'none' 51 this.container.style.display = 'none'
@@ -116,12 +55,10 @@ class StatsCard extends Component {
116 55
117 this.list = this.container.getElementsByClassName('vjs-stats-list')[0] as HTMLDivElement 56 this.list = this.container.getElementsByClassName('vjs-stats-list')[0] as HTMLDivElement
118 57
119 console.log(this.player_.qualityLevels()) 58 this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => {
120
121 this.player_.on('p2pInfo', (event: any, data: PlayerNetworkInfo) => {
122 if (!data) return // HTTP fallback 59 if (!data) return // HTTP fallback
123 60
124 this.source = data.source 61 this.mode = data.source
125 62
126 const p2pStats = data.p2p 63 const p2pStats = data.p2p
127 const httpStats = data.http 64 const httpStats = data.http
@@ -131,7 +68,7 @@ class StatsCard extends Component {
131 this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ') 68 this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ')
132 this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded).join(' ') 69 this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded).join(' ')
133 this.playerNetworkInfo.numPeers = p2pStats.numPeers 70 this.playerNetworkInfo.numPeers = p2pStats.numPeers
134 this.playerNetworkInfo.averageBandwidth = bytes(getAverageBandwidthInStore() || p2pStats.downloaded + httpStats.downloaded).join(' ') 71 this.playerNetworkInfo.averageBandwidth = bytes(data.bandwidthEstimate).join(' ') + '/s'
135 72
136 if (data.source === 'p2p-media-loader') { 73 if (data.source === 'p2p-media-loader') {
137 this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ') 74 this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ')
@@ -143,37 +80,187 @@ class StatsCard extends Component {
143 } 80 }
144 81
145 toggle () { 82 toggle () {
146 this.update 83 this.updateInterval
147 ? this.hide() 84 ? this.hide()
148 : this.show() 85 : this.show()
149 } 86 }
150 87
151 show (options?: StatsCardOptions) { 88 show () {
152 if (options) this.options_ = options
153
154 let metadata = {}
155
156 this.container.style.display = 'block' 89 this.container.style.display = 'block'
157 this.update = setInterval(async () => { 90 this.updateInterval = setInterval(async () => {
158 try { 91 try {
159 if (this.source === 'webtorrent') { 92 const options = this.mode === 'webtorrent'
160 const progress = this.player_.webtorrent().getTorrent()?.progress 93 ? await this.buildWebTorrentOptions()
161 const videoFile = this.player_.webtorrent().getCurrentVideoFile() 94 : await this.buildHLSOptions()
162 videoFile.metadata = metadata[videoFile.fileUrl] = videoFile.metadata || metadata[videoFile.fileUrl] || videoFile.metadataUrl && await fetch(videoFile.metadataUrl).then(res => res.json()) 95
163 this.list.innerHTML = getListTemplate(this.options_, this.player_, { playerNetworkInfo: this.playerNetworkInfo, videoFile, progress }) 96 this.list.innerHTML = this.getListTemplate(options)
164 } else { 97 } catch (err) {
165 this.list.innerHTML = getListTemplate(this.options_, this.player_, { playerNetworkInfo: this.playerNetworkInfo }) 98 console.error('Cannot update stats.', err)
166 } 99 clearInterval(this.updateInterval)
167 } catch (e) {
168 clearInterval(this.update)
169 } 100 }
170 }, this.interval) 101 }, this.intervalMs)
171 } 102 }
172 103
173 hide () { 104 hide () {
174 clearInterval(this.update) 105 clearInterval(this.updateInterval)
175 this.container.style.display = 'none' 106 this.container.style.display = 'none'
176 } 107 }
108
109 private async buildHLSOptions () {
110 const p2pMediaLoader = this.player_.p2pMediaLoader()
111 const level = p2pMediaLoader.getCurrentLevel()
112
113 const codecs = level?.videoCodec || level?.audioCodec
114 ? `${level?.videoCodec || ''} / ${level?.audioCodec || ''}`
115 : undefined
116
117 const resolution = `${level?.height}p${level?.attrs['FRAME-RATE'] || ''}`
118 const buffer = this.timeRangesToString(this.player().buffered())
119
120 let progress: number
121 let latency: string
122
123 if (this.options_.videoIsLive) {
124 latency = secondsToTime(p2pMediaLoader.getLiveLatency())
125 } else {
126 progress = this.player().bufferedPercent()
127 }
128
129 return {
130 playerNetworkInfo: this.playerNetworkInfo,
131 resolution,
132 codecs,
133 buffer,
134 latency,
135 progress
136 }
137 }
138
139 private async buildWebTorrentOptions () {
140 const videoFile = this.player_.webtorrent().getCurrentVideoFile()
141
142 if (!this.metadataStore[videoFile.fileUrl]) {
143 this.metadataStore[videoFile.fileUrl] = await fetch(videoFile.metadataUrl).then(res => res.json())
144 }
145
146 const metadata = this.metadataStore[videoFile.fileUrl]
147
148 let colorSpace = 'unknown'
149 let codecs = 'unknown'
150
151 if (metadata?.streams[0]) {
152 const stream = metadata.streams[0]
153
154 colorSpace = stream['color_space'] !== 'unknown'
155 ? stream['color_space']
156 : 'bt709'
157
158 codecs = stream['codec_name'] || 'avc1'
159 }
160
161 const resolution = videoFile?.resolution.label + videoFile?.fps
162 const buffer = this.timeRangesToString(this.player().buffered())
163 const progress = this.player_.webtorrent().getTorrent()?.progress
164
165 return {
166 playerNetworkInfo: this.playerNetworkInfo,
167 progress,
168 colorSpace,
169 codecs,
170 resolution,
171 buffer
172 }
173 }
174
175 private getListTemplate (options: {
176 playerNetworkInfo: PlayerNetworkInfo
177 progress: number
178 codecs: string
179 resolution: string
180 buffer: string
181
182 latency?: string
183 colorSpace?: string
184 }) {
185 const { playerNetworkInfo, progress, colorSpace, codecs, resolution, buffer, latency } = options
186 const player = this.player()
187
188 const videoQuality: VideoPlaybackQuality = player.getVideoPlaybackQuality()
189 const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
190 const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
191 const pr = (window.devicePixelRatio || 1).toFixed(2)
192 const frames = `${vw}x${vh}*${pr} / ${videoQuality.droppedVideoFrames} dropped of ${videoQuality.totalVideoFrames}`
193
194 const duration = player.duration()
195
196 let volume = `${player.volume() * 100}`
197 if (player.muted()) volume += ' (muted)'
198
199 const networkActivity = playerNetworkInfo.downloadSpeed
200 ? `${playerNetworkInfo.downloadSpeed} &dArr; / ${playerNetworkInfo.uploadSpeed} &uArr;`
201 : undefined
202
203 const totalTransferred = playerNetworkInfo.totalDownloaded
204 ? `${playerNetworkInfo.totalDownloaded} &dArr; / ${playerNetworkInfo.totalUploaded} &uArr;`
205 : undefined
206 const downloadBreakdown = playerNetworkInfo.downloadedFromServer
207 ? `${playerNetworkInfo.downloadedFromServer} from server · ${playerNetworkInfo.downloadedFromPeers} from peers`
208 : undefined
209
210 const bufferProgress = progress !== undefined
211 ? `${(progress * 100).toFixed(1)}% (${(progress * duration).toFixed(1)}s)`
212 : undefined
213
214 return `
215 ${this.buildElement(player.localize('Video UUID'), this.options_.videoUUID)}
216
217 ${this.buildElement(player.localize('Viewport / Frames'), frames)}
218
219 ${this.buildElement(player.localize('Resolution'), resolution)}
220
221 ${this.buildElement(player.localize('Volume'), volume)}
222
223 ${this.buildElement(player.localize('Codecs'), codecs)}
224 ${this.buildElement(player.localize('Color'), colorSpace)}
225
226 ${this.buildElement(player.localize('Connection Speed'), playerNetworkInfo.averageBandwidth)}
227
228 ${this.buildElement(player.localize('Network Activity'), networkActivity)}
229 ${this.buildElement(player.localize('Total Transfered'), totalTransferred)}
230 ${this.buildElement(player.localize('Download Breakdown'), downloadBreakdown)}
231
232 ${this.buildElement(player.localize('Buffer Progress'), bufferProgress)}
233 ${this.buildElement(player.localize('Buffer State'), buffer)}
234
235 ${this.buildElement(player.localize('Live Latency'), latency)}
236 `
237 }
238
239 private getMainTemplate () {
240 return `
241 <button class="vjs-stats-close" tabindex=0 aria-label="Close stats" title="Close stats">[x]</button>
242 <div class="vjs-stats-list"></div>
243 `
244 }
245
246 private buildElement (label: string, value?: string) {
247 if (!value) return ''
248
249 return `<div><div>${label}</div><span>${value}</span></div>`
250 }
251
252 private timeRangesToString (r: videojs.TimeRange) {
253 let result = ''
254
255 for (let i = 0; i < r.length; i++) {
256 const start = Math.floor(r.start(i))
257 const end = Math.floor(r.end(i))
258
259 result += `[${secondsToTime(start)}, ${secondsToTime(end)}] `
260 }
261
262 return result
263 }
177} 264}
178 265
179videojs.registerComponent('StatsCard', StatsCard) 266videojs.registerComponent('StatsCard', StatsCard)