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