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