]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/assets/player/shared/stats/stats-card.ts
Reorganize player manager options builder
[github/Chocobozzz/PeerTube.git] / client / src / assets / player / shared / stats / stats-card.ts
1 import videojs from 'video.js'
2 import { secondsToTime } from '@shared/core-utils'
3 import { PlayerNetworkInfo as EventPlayerNetworkInfo } from '../../types'
4 import { bytes } from '../common'
5
6 interface StatsCardOptions extends videojs.ComponentOptions {
7 videoUUID: string
8 videoIsLive: boolean
9 mode: 'webtorrent' | 'p2p-media-loader'
10 p2pEnabled: boolean
11 }
12
13 interface PlayerNetworkInfo {
14 downloadSpeed?: string
15 uploadSpeed?: string
16 totalDownloaded?: string
17 totalUploaded?: string
18 numPeers?: number
19 averageBandwidth?: string
20
21 downloadedFromServer?: string
22 downloadedFromPeers?: string
23 }
24
25 const Component = videojs.getComponent('Component')
26 class StatsCard extends Component {
27 options_: StatsCardOptions
28
29 container: HTMLDivElement
30
31 list: HTMLDivElement
32 closeButton: HTMLElement
33
34 updateInterval: any
35
36 mode: 'webtorrent' | 'p2p-media-loader'
37
38 metadataStore: any = {}
39
40 intervalMs = 300
41 playerNetworkInfo: PlayerNetworkInfo = {}
42
43 createEl () {
44 const container = super.createEl('div', {
45 className: 'vjs-stats-content',
46 innerHTML: this.getMainTemplate()
47 }) as HTMLDivElement
48 this.container = container
49 this.container.style.display = 'none'
50
51 this.closeButton = this.container.getElementsByClassName('vjs-stats-close')[0] as HTMLElement
52 this.closeButton.onclick = () => this.hide()
53
54 this.list = this.container.getElementsByClassName('vjs-stats-list')[0] as HTMLDivElement
55
56 this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => {
57 if (!data) return // HTTP fallback
58
59 this.mode = data.source
60
61 const p2pStats = data.p2p
62 const httpStats = data.http
63
64 this.playerNetworkInfo.downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed).join(' ')
65 this.playerNetworkInfo.uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed).join(' ')
66 this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ')
67 this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded).join(' ')
68 this.playerNetworkInfo.numPeers = p2pStats.numPeers
69 this.playerNetworkInfo.averageBandwidth = bytes(data.bandwidthEstimate).join(' ') + '/s'
70
71 if (data.source === 'p2p-media-loader') {
72 this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ')
73 this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats.downloaded).join(' ')
74 }
75 })
76
77 return container
78 }
79
80 toggle () {
81 if (this.updateInterval) this.hide()
82 else this.show()
83 }
84
85 show () {
86 this.container.style.display = 'block'
87 this.updateInterval = setInterval(async () => {
88 try {
89 const options = this.mode === 'p2p-media-loader'
90 ? this.buildHLSOptions()
91 : await this.buildWebTorrentOptions() // Default
92
93 this.list.innerHTML = this.getListTemplate(options)
94 } catch (err) {
95 console.error('Cannot update stats.', err)
96 clearInterval(this.updateInterval)
97 }
98 }, this.intervalMs)
99 }
100
101 hide () {
102 clearInterval(this.updateInterval)
103 this.container.style.display = 'none'
104 }
105
106 private buildHLSOptions () {
107 const p2pMediaLoader = this.player_.p2pMediaLoader()
108 const level = p2pMediaLoader.getCurrentLevel()
109
110 const codecs = level?.videoCodec || level?.audioCodec
111 ? `${level?.videoCodec || ''} / ${level?.audioCodec || ''}`
112 : undefined
113
114 const resolution = `${level?.height}p${level?.attrs['FRAME-RATE'] || ''}`
115 const buffer = this.timeRangesToString(this.player().buffered())
116
117 let progress: number
118 let latency: string
119
120 if (this.options_.videoIsLive) {
121 latency = secondsToTime(p2pMediaLoader.getLiveLatency())
122 } else {
123 progress = this.player().bufferedPercent()
124 }
125
126 return {
127 playerNetworkInfo: this.playerNetworkInfo,
128 resolution,
129 codecs,
130 buffer,
131 latency,
132 progress
133 }
134 }
135
136 private async buildWebTorrentOptions () {
137 const videoFile = this.player_.webtorrent().getCurrentVideoFile()
138
139 if (!this.metadataStore[videoFile.fileUrl]) {
140 this.metadataStore[videoFile.fileUrl] = await fetch(videoFile.metadataUrl).then(res => res.json())
141 }
142
143 const metadata = this.metadataStore[videoFile.fileUrl]
144
145 let colorSpace = 'unknown'
146 let codecs = 'unknown'
147
148 if (metadata?.streams[0]) {
149 const stream = metadata.streams[0]
150
151 colorSpace = stream['color_space'] !== 'unknown'
152 ? stream['color_space']
153 : 'bt709'
154
155 codecs = stream['codec_name'] || 'avc1'
156 }
157
158 const resolution = videoFile?.resolution.label + videoFile?.fps
159 const buffer = this.timeRangesToString(this.player().buffered())
160 const progress = this.player_.webtorrent().getTorrent()?.progress
161
162 return {
163 playerNetworkInfo: this.playerNetworkInfo,
164 progress,
165 colorSpace,
166 codecs,
167 resolution,
168 buffer
169 }
170 }
171
172 private getListTemplate (options: {
173 playerNetworkInfo: PlayerNetworkInfo
174 progress: number
175 codecs: string
176 resolution: string
177 buffer: string
178
179 latency?: string
180 colorSpace?: string
181 }) {
182 const { playerNetworkInfo, progress, colorSpace, codecs, resolution, buffer, latency } = options
183 const player = this.player()
184
185 const videoQuality: VideoPlaybackQuality = player.getVideoPlaybackQuality()
186 const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
187 const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
188 const pr = (window.devicePixelRatio || 1).toFixed(2)
189 const frames = `${vw}x${vh}*${pr} / ${videoQuality.droppedVideoFrames} dropped of ${videoQuality.totalVideoFrames}`
190
191 const duration = player.duration()
192
193 let volume = `${Math.round(player.volume() * 100)}`
194 if (player.muted()) volume += ' (muted)'
195
196 const networkActivity = playerNetworkInfo.downloadSpeed
197 ? `${playerNetworkInfo.downloadSpeed} ⇓ / ${playerNetworkInfo.uploadSpeed} ⇑`
198 : undefined
199
200 const totalTransferred = playerNetworkInfo.totalDownloaded
201 ? `${playerNetworkInfo.totalDownloaded} ⇓ / ${playerNetworkInfo.totalUploaded} ⇑`
202 : undefined
203 const downloadBreakdown = playerNetworkInfo.downloadedFromServer
204 ? `${playerNetworkInfo.downloadedFromServer} from servers ยท ${playerNetworkInfo.downloadedFromPeers} from peers`
205 : undefined
206
207 const bufferProgress = progress !== undefined
208 ? `${(progress * 100).toFixed(1)}% (${(progress * duration).toFixed(1)}s)`
209 : undefined
210
211 return `
212 ${this.buildElement(player.localize('Player mode'), this.mode || 'HTTP')}
213 ${this.buildElement(player.localize('P2P'), player.localize(this.options_.p2pEnabled ? 'enabled' : 'disabled'))}
214
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 }
264 }
265
266 videojs.registerComponent('StatsCard', StatsCard)
267
268 export {
269 StatsCard,
270 StatsCardOptions
271 }