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