]>
Commit | Line | Data |
---|---|---|
ff563914 | 1 | import videojs from 'video.js' |
42b40636 | 2 | import { logger } from '@root-helpers/logger' |
15a7eafb | 3 | import { secondsToTime } from '@shared/core-utils' |
57d65032 C |
4 | import { PlayerNetworkInfo as EventPlayerNetworkInfo } from '../../types' |
5 | import { bytes } from '../common' | |
ff563914 RK |
6 | |
7 | interface StatsCardOptions extends videojs.ComponentOptions { | |
4e11d8f3 C |
8 | videoUUID: string |
9 | videoIsLive: boolean | |
10 | mode: 'webtorrent' | 'p2p-media-loader' | |
95765067 | 11 | p2pEnabled: boolean |
ff563914 RK |
12 | } |
13 | ||
4e11d8f3 C |
14 | interface PlayerNetworkInfo { |
15 | downloadSpeed?: string | |
16 | uploadSpeed?: string | |
17 | totalDownloaded?: string | |
18 | totalUploaded?: string | |
19 | numPeers?: number | |
20 | averageBandwidth?: string | |
ff563914 | 21 | |
4e11d8f3 C |
22 | downloadedFromServer?: string |
23 | downloadedFromPeers?: string | |
ff563914 RK |
24 | } |
25 | ||
68e72ba9 C |
26 | interface InfoElement { |
27 | root: HTMLElement | |
28 | value: HTMLElement | |
29 | } | |
30 | ||
ff563914 RK |
31 | const Component = videojs.getComponent('Component') |
32 | class StatsCard extends Component { | |
33 | options_: StatsCardOptions | |
4e11d8f3 | 34 | |
4e11d8f3 C |
35 | updateInterval: any |
36 | ||
37 | mode: 'webtorrent' | 'p2p-media-loader' | |
38 | ||
39 | metadataStore: any = {} | |
40 | ||
41 | intervalMs = 300 | |
42 | playerNetworkInfo: PlayerNetworkInfo = {} | |
ff563914 | 43 | |
68e72ba9 C |
44 | private containerEl: HTMLDivElement |
45 | private infoListEl: HTMLDivElement | |
46 | ||
47 | private playerMode: InfoElement | |
48 | private p2p: InfoElement | |
49 | private uuid: InfoElement | |
50 | private viewport: InfoElement | |
51 | private resolution: InfoElement | |
52 | private volume: InfoElement | |
53 | private codecs: InfoElement | |
54 | private color: InfoElement | |
55 | private connection: InfoElement | |
56 | ||
57 | private network: InfoElement | |
58 | private transferred: InfoElement | |
59 | private download: InfoElement | |
60 | ||
61 | private bufferProgress: InfoElement | |
62 | private bufferState: InfoElement | |
63 | ||
64 | private liveLatency: InfoElement | |
65 | ||
ff563914 | 66 | createEl () { |
68e72ba9 C |
67 | this.containerEl = videojs.dom.createEl('div', { |
68 | className: 'vjs-stats-content' | |
69 | }) as HTMLDivElement | |
70 | this.containerEl.style.display = 'none' | |
71 | ||
72 | this.infoListEl = videojs.dom.createEl('div', { | |
73 | className: 'vjs-stats-list' | |
ff563914 | 74 | }) as HTMLDivElement |
ff563914 | 75 | |
68e72ba9 C |
76 | const closeButton = videojs.dom.createEl('button', { |
77 | className: 'vjs-stats-close', | |
78 | tabindex: '0', | |
79 | title: 'Close stats', | |
80 | innerText: '[x]' | |
81 | }, { 'aria-label': 'Close stats' }) as HTMLElement | |
82 | closeButton.onclick = () => this.hide() | |
ff563914 | 83 | |
68e72ba9 C |
84 | this.containerEl.appendChild(closeButton) |
85 | this.containerEl.appendChild(this.infoListEl) | |
86 | ||
87 | this.populateInfoBlocks() | |
ff563914 | 88 | |
4e11d8f3 | 89 | this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => { |
ff563914 RK |
90 | if (!data) return // HTTP fallback |
91 | ||
4e11d8f3 | 92 | this.mode = data.source |
ff563914 RK |
93 | |
94 | const p2pStats = data.p2p | |
95 | const httpStats = data.http | |
96 | ||
97 | this.playerNetworkInfo.downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed).join(' ') | |
fd3c2e87 | 98 | this.playerNetworkInfo.uploadSpeed = bytes(p2pStats.uploadSpeed).join(' ') |
ff563914 | 99 | this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ') |
fd3c2e87 | 100 | this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded).join(' ') |
ff563914 | 101 | this.playerNetworkInfo.numPeers = p2pStats.numPeers |
4e11d8f3 | 102 | this.playerNetworkInfo.averageBandwidth = bytes(data.bandwidthEstimate).join(' ') + '/s' |
ff563914 RK |
103 | |
104 | if (data.source === 'p2p-media-loader') { | |
105 | this.playerNetworkInfo.downloadedFromServer = bytes(httpStats.downloaded).join(' ') | |
106 | this.playerNetworkInfo.downloadedFromPeers = bytes(p2pStats.downloaded).join(' ') | |
107 | } | |
108 | }) | |
109 | ||
68e72ba9 | 110 | return this.containerEl |
ff563914 RK |
111 | } |
112 | ||
113 | toggle () { | |
9df52d66 C |
114 | if (this.updateInterval) this.hide() |
115 | else this.show() | |
ff563914 RK |
116 | } |
117 | ||
4e11d8f3 | 118 | show () { |
68e72ba9 C |
119 | this.containerEl.style.display = 'block' |
120 | ||
4e11d8f3 | 121 | this.updateInterval = setInterval(async () => { |
ff563914 | 122 | try { |
db0159c7 | 123 | const options = this.mode === 'p2p-media-loader' |
98ab5dc8 | 124 | ? this.buildHLSOptions() |
db0159c7 | 125 | : await this.buildWebTorrentOptions() // Default |
4e11d8f3 | 126 | |
68e72ba9 | 127 | this.populateInfoValues(options) |
4e11d8f3 | 128 | } catch (err) { |
42b40636 | 129 | logger.error('Cannot update stats.', err) |
4e11d8f3 | 130 | clearInterval(this.updateInterval) |
ff563914 | 131 | } |
4e11d8f3 | 132 | }, this.intervalMs) |
ff563914 RK |
133 | } |
134 | ||
135 | hide () { | |
4e11d8f3 | 136 | clearInterval(this.updateInterval) |
68e72ba9 | 137 | this.containerEl.style.display = 'none' |
ff563914 | 138 | } |
4e11d8f3 | 139 | |
98ab5dc8 | 140 | private buildHLSOptions () { |
4e11d8f3 C |
141 | const p2pMediaLoader = this.player_.p2pMediaLoader() |
142 | const level = p2pMediaLoader.getCurrentLevel() | |
143 | ||
144 | const codecs = level?.videoCodec || level?.audioCodec | |
145 | ? `${level?.videoCodec || ''} / ${level?.audioCodec || ''}` | |
146 | : undefined | |
147 | ||
148 | const resolution = `${level?.height}p${level?.attrs['FRAME-RATE'] || ''}` | |
149 | const buffer = this.timeRangesToString(this.player().buffered()) | |
150 | ||
151 | let progress: number | |
152 | let latency: string | |
153 | ||
154 | if (this.options_.videoIsLive) { | |
155 | latency = secondsToTime(p2pMediaLoader.getLiveLatency()) | |
156 | } else { | |
157 | progress = this.player().bufferedPercent() | |
158 | } | |
159 | ||
160 | return { | |
161 | playerNetworkInfo: this.playerNetworkInfo, | |
162 | resolution, | |
163 | codecs, | |
164 | buffer, | |
165 | latency, | |
166 | progress | |
167 | } | |
168 | } | |
169 | ||
170 | private async buildWebTorrentOptions () { | |
171 | const videoFile = this.player_.webtorrent().getCurrentVideoFile() | |
172 | ||
173 | if (!this.metadataStore[videoFile.fileUrl]) { | |
174 | this.metadataStore[videoFile.fileUrl] = await fetch(videoFile.metadataUrl).then(res => res.json()) | |
175 | } | |
176 | ||
177 | const metadata = this.metadataStore[videoFile.fileUrl] | |
178 | ||
179 | let colorSpace = 'unknown' | |
180 | let codecs = 'unknown' | |
181 | ||
182 | if (metadata?.streams[0]) { | |
183 | const stream = metadata.streams[0] | |
184 | ||
185 | colorSpace = stream['color_space'] !== 'unknown' | |
186 | ? stream['color_space'] | |
187 | : 'bt709' | |
188 | ||
189 | codecs = stream['codec_name'] || 'avc1' | |
190 | } | |
191 | ||
192 | const resolution = videoFile?.resolution.label + videoFile?.fps | |
193 | const buffer = this.timeRangesToString(this.player().buffered()) | |
194 | const progress = this.player_.webtorrent().getTorrent()?.progress | |
195 | ||
196 | return { | |
197 | playerNetworkInfo: this.playerNetworkInfo, | |
198 | progress, | |
199 | colorSpace, | |
200 | codecs, | |
201 | resolution, | |
202 | buffer | |
203 | } | |
204 | } | |
205 | ||
68e72ba9 C |
206 | private populateInfoBlocks () { |
207 | this.playerMode = this.buildInfoRow(this.player().localize('Player mode')) | |
208 | this.p2p = this.buildInfoRow(this.player().localize('P2P')) | |
209 | this.uuid = this.buildInfoRow(this.player().localize('Video UUID')) | |
210 | this.viewport = this.buildInfoRow(this.player().localize('Viewport / Frames')) | |
211 | this.resolution = this.buildInfoRow(this.player().localize('Resolution')) | |
212 | this.volume = this.buildInfoRow(this.player().localize('Volume')) | |
213 | this.codecs = this.buildInfoRow(this.player().localize('Codecs')) | |
214 | this.color = this.buildInfoRow(this.player().localize('Color')) | |
215 | this.connection = this.buildInfoRow(this.player().localize('Connection Speed')) | |
216 | ||
217 | this.network = this.buildInfoRow(this.player().localize('Network Activity')) | |
218 | this.transferred = this.buildInfoRow(this.player().localize('Total Transfered')) | |
219 | this.download = this.buildInfoRow(this.player().localize('Download Breakdown')) | |
220 | ||
221 | this.bufferProgress = this.buildInfoRow(this.player().localize('Buffer Progress')) | |
222 | this.bufferState = this.buildInfoRow(this.player().localize('Buffer State')) | |
223 | ||
224 | this.liveLatency = this.buildInfoRow(this.player().localize('Live Latency')) | |
225 | ||
226 | this.infoListEl.appendChild(this.playerMode.root) | |
227 | this.infoListEl.appendChild(this.p2p.root) | |
228 | this.infoListEl.appendChild(this.uuid.root) | |
229 | this.infoListEl.appendChild(this.viewport.root) | |
230 | this.infoListEl.appendChild(this.resolution.root) | |
231 | this.infoListEl.appendChild(this.volume.root) | |
232 | this.infoListEl.appendChild(this.codecs.root) | |
233 | this.infoListEl.appendChild(this.color.root) | |
234 | this.infoListEl.appendChild(this.connection.root) | |
235 | this.infoListEl.appendChild(this.network.root) | |
236 | this.infoListEl.appendChild(this.transferred.root) | |
237 | this.infoListEl.appendChild(this.download.root) | |
238 | this.infoListEl.appendChild(this.bufferProgress.root) | |
239 | this.infoListEl.appendChild(this.bufferState.root) | |
240 | this.infoListEl.appendChild(this.liveLatency.root) | |
241 | } | |
242 | ||
243 | private populateInfoValues (options: { | |
4e11d8f3 C |
244 | playerNetworkInfo: PlayerNetworkInfo |
245 | progress: number | |
246 | codecs: string | |
247 | resolution: string | |
248 | buffer: string | |
249 | ||
250 | latency?: string | |
251 | colorSpace?: string | |
252 | }) { | |
253 | const { playerNetworkInfo, progress, colorSpace, codecs, resolution, buffer, latency } = options | |
254 | const player = this.player() | |
255 | ||
256 | const videoQuality: VideoPlaybackQuality = player.getVideoPlaybackQuality() | |
257 | const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0) | |
258 | const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0) | |
259 | const pr = (window.devicePixelRatio || 1).toFixed(2) | |
260 | const frames = `${vw}x${vh}*${pr} / ${videoQuality.droppedVideoFrames} dropped of ${videoQuality.totalVideoFrames}` | |
261 | ||
262 | const duration = player.duration() | |
263 | ||
b76db2ff | 264 | let volume = `${Math.round(player.volume() * 100)}` |
4e11d8f3 C |
265 | if (player.muted()) volume += ' (muted)' |
266 | ||
267 | const networkActivity = playerNetworkInfo.downloadSpeed | |
268 | ? `${playerNetworkInfo.downloadSpeed} ⇓ / ${playerNetworkInfo.uploadSpeed} ⇑` | |
269 | : undefined | |
270 | ||
271 | const totalTransferred = playerNetworkInfo.totalDownloaded | |
272 | ? `${playerNetworkInfo.totalDownloaded} ⇓ / ${playerNetworkInfo.totalUploaded} ⇑` | |
273 | : undefined | |
274 | const downloadBreakdown = playerNetworkInfo.downloadedFromServer | |
95765067 | 275 | ? `${playerNetworkInfo.downloadedFromServer} from servers ยท ${playerNetworkInfo.downloadedFromPeers} from peers` |
4e11d8f3 C |
276 | : undefined |
277 | ||
278 | const bufferProgress = progress !== undefined | |
279 | ? `${(progress * 100).toFixed(1)}% (${(progress * duration).toFixed(1)}s)` | |
280 | : undefined | |
281 | ||
68e72ba9 C |
282 | this.setInfoValue(this.playerMode, this.mode || 'HTTP') |
283 | this.setInfoValue(this.p2p, player.localize(this.options_.p2pEnabled ? 'enabled' : 'disabled')) | |
284 | this.setInfoValue(this.uuid, this.options_.videoUUID) | |
4e11d8f3 | 285 | |
68e72ba9 C |
286 | this.setInfoValue(this.viewport, frames) |
287 | this.setInfoValue(this.resolution, resolution) | |
288 | this.setInfoValue(this.volume, volume) | |
289 | this.setInfoValue(this.codecs, codecs) | |
290 | this.setInfoValue(this.color, colorSpace) | |
291 | this.setInfoValue(this.connection, playerNetworkInfo.averageBandwidth) | |
4e11d8f3 | 292 | |
68e72ba9 C |
293 | this.setInfoValue(this.network, networkActivity) |
294 | this.setInfoValue(this.transferred, totalTransferred) | |
295 | this.setInfoValue(this.download, downloadBreakdown) | |
4e11d8f3 | 296 | |
68e72ba9 C |
297 | this.setInfoValue(this.bufferProgress, bufferProgress) |
298 | this.setInfoValue(this.bufferState, buffer) | |
4e11d8f3 | 299 | |
68e72ba9 C |
300 | this.setInfoValue(this.liveLatency, latency) |
301 | } | |
4e11d8f3 | 302 | |
68e72ba9 C |
303 | private setInfoValue (el: InfoElement, value: string) { |
304 | if (!value) { | |
305 | el.root.style.display = 'none' | |
306 | return | |
307 | } | |
4e11d8f3 | 308 | |
68e72ba9 | 309 | el.root.style.display = 'block' |
4e11d8f3 | 310 | |
68e72ba9 C |
311 | if (el.value.innerHTML === value) return |
312 | el.value.innerHTML = value | |
4e11d8f3 C |
313 | } |
314 | ||
68e72ba9 C |
315 | private buildInfoRow (labelText: string, valueHTML?: string) { |
316 | const root = videojs.dom.createEl('div') as HTMLElement | |
317 | root.style.display = 'none' | |
318 | ||
319 | const label = videojs.dom.createEl('div', { innerText: labelText }) as HTMLElement | |
320 | const value = videojs.dom.createEl('span', { innerHTML: valueHTML }) as HTMLElement | |
4e11d8f3 | 321 | |
68e72ba9 C |
322 | root.appendChild(label) |
323 | root.appendChild(value) | |
4e11d8f3 | 324 | |
68e72ba9 | 325 | return { root, value } |
4e11d8f3 C |
326 | } |
327 | ||
328 | private timeRangesToString (r: videojs.TimeRange) { | |
329 | let result = '' | |
330 | ||
331 | for (let i = 0; i < r.length; i++) { | |
332 | const start = Math.floor(r.start(i)) | |
333 | const end = Math.floor(r.end(i)) | |
334 | ||
335 | result += `[${secondsToTime(start)}, ${secondsToTime(end)}] ` | |
336 | } | |
337 | ||
338 | return result | |
339 | } | |
ff563914 RK |
340 | } |
341 | ||
342 | videojs.registerComponent('StatsCard', StatsCard) | |
343 | ||
344 | export { | |
345 | StatsCard, | |
346 | StatsCardOptions | |
347 | } |