]>
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' | |
ff563914 RK |
10 | } |
11 | ||
4e11d8f3 C |
12 | interface 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 | ||
24 | const Component = videojs.getComponent('Component') | |
25 | class 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 | 41 | |
ff563914 RK |
42 | createEl () { |
43 | const container = super.createEl('div', { | |
44 | className: 'vjs-stats-content', | |
4e11d8f3 | 45 | innerHTML: this.getMainTemplate() |
ff563914 RK |
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 | ||
4e11d8f3 | 55 | this.player_.on('p2pInfo', (event: any, data: EventPlayerNetworkInfo) => { |
ff563914 RK |
56 | if (!data) return // HTTP fallback |
57 | ||
4e11d8f3 | 58 | this.mode = data.source |
ff563914 RK |
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 | |
4e11d8f3 | 68 | this.playerNetworkInfo.averageBandwidth = bytes(data.bandwidthEstimate).join(' ') + '/s' |
ff563914 RK |
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 () { | |
9df52d66 C |
80 | if (this.updateInterval) this.hide() |
81 | else this.show() | |
ff563914 RK |
82 | } |
83 | ||
4e11d8f3 | 84 | show () { |
ff563914 | 85 | this.container.style.display = 'block' |
4e11d8f3 | 86 | this.updateInterval = setInterval(async () => { |
ff563914 | 87 | try { |
db0159c7 | 88 | const options = this.mode === 'p2p-media-loader' |
98ab5dc8 | 89 | ? this.buildHLSOptions() |
db0159c7 | 90 | : await this.buildWebTorrentOptions() // Default |
4e11d8f3 C |
91 | |
92 | this.list.innerHTML = this.getListTemplate(options) | |
93 | } catch (err) { | |
94 | console.error('Cannot update stats.', err) | |
95 | clearInterval(this.updateInterval) | |
ff563914 | 96 | } |
4e11d8f3 | 97 | }, this.intervalMs) |
ff563914 RK |
98 | } |
99 | ||
100 | hide () { | |
4e11d8f3 | 101 | clearInterval(this.updateInterval) |
ff563914 RK |
102 | this.container.style.display = 'none' |
103 | } | |
4e11d8f3 | 104 | |
98ab5dc8 | 105 | private buildHLSOptions () { |
4e11d8f3 C |
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 | ||
b76db2ff | 192 | let volume = `${Math.round(player.volume() * 100)}` |
4e11d8f3 C |
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 ` | |
db0159c7 | 211 | ${this.buildElement(player.localize('Player mode'), this.mode || 'HTTP')} |
a45050e0 | 212 | |
4e11d8f3 C |
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 | } | |
ff563914 RK |
262 | } |
263 | ||
264 | videojs.registerComponent('StatsCard', StatsCard) | |
265 | ||
266 | export { | |
267 | StatsCard, | |
268 | StatsCardOptions | |
269 | } |