]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - client/src/assets/player/stats/stats-card.ts
Add player mode to player stats
[github/Chocobozzz/PeerTube.git] / client / src / assets / player / stats / stats-card.ts
CommitLineData
ff563914 1import videojs from 'video.js'
4e11d8f3
C
2import { PlayerNetworkInfo as EventPlayerNetworkInfo } from '../peertube-videojs-typings'
3import { bytes, secondsToTime } from '../utils'
ff563914
RK
4
5interface StatsCardOptions extends videojs.ComponentOptions {
4e11d8f3
C
6 videoUUID: string
7 videoIsLive: boolean
8 mode: 'webtorrent' | 'p2p-media-loader'
ff563914
RK
9}
10
4e11d8f3
C
11interface PlayerNetworkInfo {
12 downloadSpeed?: string
13 uploadSpeed?: string
14 totalDownloaded?: string
15 totalUploaded?: string
16 numPeers?: number
17 averageBandwidth?: string
ff563914 18
4e11d8f3
C
19 downloadedFromServer?: string
20 downloadedFromPeers?: string
ff563914
RK
21}
22
23const Component = videojs.getComponent('Component')
24class StatsCard extends Component {
25 options_: StatsCardOptions
4e11d8f3 26
ff563914 27 container: HTMLDivElement
4e11d8f3 28
ff563914
RK
29 list: HTMLDivElement
30 closeButton: HTMLElement
ff563914 31
4e11d8f3
C
32 updateInterval: any
33
34 mode: 'webtorrent' | 'p2p-media-loader'
35
36 metadataStore: any = {}
37
38 intervalMs = 300
39 playerNetworkInfo: PlayerNetworkInfo = {}
ff563914
RK
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',
4e11d8f3 48 innerHTML: this.getMainTemplate()
ff563914
RK
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
4e11d8f3 58 this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => {
ff563914
RK
59 if (!data) return // HTTP fallback
60
4e11d8f3 61 this.mode = data.source
ff563914
RK
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
4e11d8f3 71 this.playerNetworkInfo.averageBandwidth = bytes(data.bandwidthEstimate).join(' ') + '/s'
ff563914
RK
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 () {
4e11d8f3 83 this.updateInterval
ff563914
RK
84 ? this.hide()
85 : this.show()
86 }
87
4e11d8f3 88 show () {
ff563914 89 this.container.style.display = 'block'
4e11d8f3 90 this.updateInterval = setInterval(async () => {
ff563914 91 try {
4e11d8f3
C
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)
ff563914 100 }
4e11d8f3 101 }, this.intervalMs)
ff563914
RK
102 }
103
104 hide () {
4e11d8f3 105 clearInterval(this.updateInterval)
ff563914
RK
106 this.container.style.display = 'none'
107 }
4e11d8f3
C
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 `
a45050e0
C
215 ${this.buildElement(player.localize('Player mode'), this.options_.mode)}
216
4e11d8f3
C
217 ${this.buildElement(player.localize('Video UUID'), this.options_.videoUUID)}
218
219 ${this.buildElement(player.localize('Viewport / Frames'), frames)}
220
221 ${this.buildElement(player.localize('Resolution'), resolution)}
222
223 ${this.buildElement(player.localize('Volume'), volume)}
224
225 ${this.buildElement(player.localize('Codecs'), codecs)}
226 ${this.buildElement(player.localize('Color'), colorSpace)}
227
228 ${this.buildElement(player.localize('Connection Speed'), playerNetworkInfo.averageBandwidth)}
229
230 ${this.buildElement(player.localize('Network Activity'), networkActivity)}
231 ${this.buildElement(player.localize('Total Transfered'), totalTransferred)}
232 ${this.buildElement(player.localize('Download Breakdown'), downloadBreakdown)}
233
234 ${this.buildElement(player.localize('Buffer Progress'), bufferProgress)}
235 ${this.buildElement(player.localize('Buffer State'), buffer)}
236
237 ${this.buildElement(player.localize('Live Latency'), latency)}
238 `
239 }
240
241 private getMainTemplate () {
242 return `
243 <button class="vjs-stats-close" tabindex=0 aria-label="Close stats" title="Close stats">[x]</button>
244 <div class="vjs-stats-list"></div>
245 `
246 }
247
248 private buildElement (label: string, value?: string) {
249 if (!value) return ''
250
251 return `<div><div>${label}</div><span>${value}</span></div>`
252 }
253
254 private timeRangesToString (r: videojs.TimeRange) {
255 let result = ''
256
257 for (let i = 0; i < r.length; i++) {
258 const start = Math.floor(r.start(i))
259 const end = Math.floor(r.end(i))
260
261 result += `[${secondsToTime(start)}, ${secondsToTime(end)}] `
262 }
263
264 return result
265 }
ff563914
RK
266}
267
268videojs.registerComponent('StatsCard', StatsCard)
269
270export {
271 StatsCard,
272 StatsCardOptions
273}