]>
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 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 | ||
269 | videojs.registerComponent('StatsCard', StatsCard) | |
270 | ||
271 | export { | |
272 | StatsCard, | |
273 | StatsCardOptions | |
274 | } |