]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - client/src/assets/player/stats/stats-card.ts
f66766089e699a5103216c09b369e66963dc3597
[github/Chocobozzz/PeerTube.git] / client / src / assets / player / stats / stats-card.ts
1 import videojs from 'video.js'
2 import { PlayerNetworkInfo as EventPlayerNetworkInfo } from '../peertube-videojs-typings'
3 import { bytes, secondsToTime } from '../utils'
4
5 interface StatsCardOptions extends videojs.ComponentOptions {
6 videoUUID: string
7 videoIsLive: boolean
8 mode: 'webtorrent' | 'p2p-media-loader'
9 }
10
11 interface PlayerNetworkInfo {
12 downloadSpeed?: string
13 uploadSpeed?: string
14 totalDownloaded?: string
15 totalUploaded?: string
16 numPeers?: number
17 averageBandwidth?: string
18
19 downloadedFromServer?: string
20 downloadedFromPeers?: string
21 }
22
23 const Component = videojs.getComponent('Component')
24 class StatsCard extends Component {
25 options_: StatsCardOptions
26
27 container: HTMLDivElement
28
29 list: HTMLDivElement
30 closeButton: HTMLElement
31
32 updateInterval: any
33
34 mode: 'webtorrent' | 'p2p-media-loader'
35
36 metadataStore: any = {}
37
38 intervalMs = 300
39 playerNetworkInfo: PlayerNetworkInfo = {}
40
41 constructor (player: videojs.Player, options: StatsCardOptions) {
42 super(player, options)
43 }
44
45 createEl () {
46 const container = super.createEl('div', {
47 className: 'vjs-stats-content',
48 innerHTML: this.getMainTemplate()
49 }) as HTMLDivElement
50 this.container = container
51 this.container.style.display = 'none'
52
53 this.closeButton = this.container.getElementsByClassName('vjs-stats-close')[0] as HTMLElement
54 this.closeButton.onclick = () => this.hide()
55
56 this.list = this.container.getElementsByClassName('vjs-stats-list')[0] as HTMLDivElement
57
58 this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => {
59 if (!data) return // HTTP fallback
60
61 this.mode = data.source
62
63 const p2pStats = data.p2p
64 const httpStats = data.http
65
66 this.playerNetworkInfo.downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed).join(' ')
67 this.playerNetworkInfo.uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed).join(' ')
68 this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ')
69 this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded).join(' ')
70 this.playerNetworkInfo.numPeers = p2pStats.numPeers
71 this.playerNetworkInfo.averageBandwidth = bytes(data.bandwidthEstimate).join(' ') + '/s'
72
73 if (data.source === 'p2p-media-loader') {
74 this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ')
75 this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats.downloaded).join(' ')
76 }
77 })
78
79 return container
80 }
81
82 toggle () {
83 this.updateInterval
84 ? this.hide()
85 : this.show()
86 }
87
88 show () {
89 this.container.style.display = 'block'
90 this.updateInterval = setInterval(async () => {
91 try {
92 const options = this.mode === 'webtorrent'
93 ? await this.buildWebTorrentOptions()
94 : await this.buildHLSOptions()
95
96 this.list.innerHTML = this.getListTemplate(options)
97 } catch (err) {
98 console.error('Cannot update stats.', err)
99 clearInterval(this.updateInterval)
100 }
101 }, this.intervalMs)
102 }
103
104 hide () {
105 clearInterval(this.updateInterval)
106 this.container.style.display = 'none'
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} ⇓ / ${playerNetworkInfo.uploadSpeed} ⇑`
201 : undefined
202
203 const totalTransferred = playerNetworkInfo.totalDownloaded
204 ? `${playerNetworkInfo.totalDownloaded} ⇓ / ${playerNetworkInfo.totalUploaded} ⇑`
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 }
264 }
265
266 videojs.registerComponent('StatsCard', StatsCard)
267
268 export {
269 StatsCard,
270 StatsCardOptions
271 }