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