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