@livepeer-frameworks/player-wc 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/cjs/components/fw-dev-mode-panel.js +845 -212
  2. package/dist/cjs/components/fw-dev-mode-panel.js.map +1 -1
  3. package/dist/cjs/components/fw-dvd-logo.js +211 -0
  4. package/dist/cjs/components/fw-dvd-logo.js.map +1 -0
  5. package/dist/cjs/components/fw-idle-screen.js +641 -97
  6. package/dist/cjs/components/fw-idle-screen.js.map +1 -1
  7. package/dist/cjs/components/fw-loading-screen.js +513 -0
  8. package/dist/cjs/components/fw-loading-screen.js.map +1 -0
  9. package/dist/cjs/components/fw-player-controls.js +390 -173
  10. package/dist/cjs/components/fw-player-controls.js.map +1 -1
  11. package/dist/cjs/components/fw-player.js +506 -63
  12. package/dist/cjs/components/fw-player.js.map +1 -1
  13. package/dist/cjs/components/fw-seek-bar.js +292 -142
  14. package/dist/cjs/components/fw-seek-bar.js.map +1 -1
  15. package/dist/cjs/components/fw-settings-menu.js +208 -81
  16. package/dist/cjs/components/fw-settings-menu.js.map +1 -1
  17. package/dist/cjs/components/fw-stats-panel.js +134 -70
  18. package/dist/cjs/components/fw-stats-panel.js.map +1 -1
  19. package/dist/cjs/components/fw-stream-state-overlay.js +338 -0
  20. package/dist/cjs/components/fw-stream-state-overlay.js.map +1 -0
  21. package/dist/cjs/components/fw-subtitle-renderer.js +174 -27
  22. package/dist/cjs/components/fw-subtitle-renderer.js.map +1 -1
  23. package/dist/cjs/components/fw-thumbnail-overlay.js +161 -0
  24. package/dist/cjs/components/fw-thumbnail-overlay.js.map +1 -0
  25. package/dist/cjs/components/fw-volume-control.js +150 -69
  26. package/dist/cjs/components/fw-volume-control.js.map +1 -1
  27. package/dist/cjs/components/shared/hitmarker-audio.js +76 -0
  28. package/dist/cjs/components/shared/hitmarker-audio.js.map +1 -0
  29. package/dist/cjs/constants/media-assets.js +11 -0
  30. package/dist/cjs/constants/media-assets.js.map +1 -0
  31. package/dist/cjs/controllers/player-controller-host.js +51 -2
  32. package/dist/cjs/controllers/player-controller-host.js.map +1 -1
  33. package/dist/cjs/define.js +8 -0
  34. package/dist/cjs/define.js.map +1 -1
  35. package/dist/cjs/icons/index.js +27 -0
  36. package/dist/cjs/icons/index.js.map +1 -1
  37. package/dist/cjs/index.js +20 -0
  38. package/dist/cjs/index.js.map +1 -1
  39. package/dist/esm/components/fw-dev-mode-panel.js +846 -213
  40. package/dist/esm/components/fw-dev-mode-panel.js.map +1 -1
  41. package/dist/esm/components/fw-dvd-logo.js +211 -0
  42. package/dist/esm/components/fw-dvd-logo.js.map +1 -0
  43. package/dist/esm/components/fw-idle-screen.js +643 -99
  44. package/dist/esm/components/fw-idle-screen.js.map +1 -1
  45. package/dist/esm/components/fw-loading-screen.js +513 -0
  46. package/dist/esm/components/fw-loading-screen.js.map +1 -0
  47. package/dist/esm/components/fw-player-controls.js +391 -174
  48. package/dist/esm/components/fw-player-controls.js.map +1 -1
  49. package/dist/esm/components/fw-player.js +506 -63
  50. package/dist/esm/components/fw-player.js.map +1 -1
  51. package/dist/esm/components/fw-seek-bar.js +293 -143
  52. package/dist/esm/components/fw-seek-bar.js.map +1 -1
  53. package/dist/esm/components/fw-settings-menu.js +209 -82
  54. package/dist/esm/components/fw-settings-menu.js.map +1 -1
  55. package/dist/esm/components/fw-stats-panel.js +135 -71
  56. package/dist/esm/components/fw-stats-panel.js.map +1 -1
  57. package/dist/esm/components/fw-stream-state-overlay.js +338 -0
  58. package/dist/esm/components/fw-stream-state-overlay.js.map +1 -0
  59. package/dist/esm/components/fw-subtitle-renderer.js +175 -28
  60. package/dist/esm/components/fw-subtitle-renderer.js.map +1 -1
  61. package/dist/esm/components/fw-thumbnail-overlay.js +161 -0
  62. package/dist/esm/components/fw-thumbnail-overlay.js.map +1 -0
  63. package/dist/esm/components/fw-volume-control.js +150 -69
  64. package/dist/esm/components/fw-volume-control.js.map +1 -1
  65. package/dist/esm/components/shared/hitmarker-audio.js +74 -0
  66. package/dist/esm/components/shared/hitmarker-audio.js.map +1 -0
  67. package/dist/esm/constants/media-assets.js +8 -0
  68. package/dist/esm/constants/media-assets.js.map +1 -0
  69. package/dist/esm/controllers/player-controller-host.js +51 -2
  70. package/dist/esm/controllers/player-controller-host.js.map +1 -1
  71. package/dist/esm/define.js +8 -0
  72. package/dist/esm/define.js.map +1 -1
  73. package/dist/esm/icons/index.js +26 -2
  74. package/dist/esm/icons/index.js.map +1 -1
  75. package/dist/esm/index.js +4 -0
  76. package/dist/esm/index.js.map +1 -1
  77. package/dist/fw-player.iife.js +2097 -883
  78. package/dist/types/components/fw-dev-mode-panel.d.ts +36 -9
  79. package/dist/types/components/fw-dvd-logo.d.ts +29 -0
  80. package/dist/types/components/fw-idle-screen.d.ts +36 -0
  81. package/dist/types/components/fw-loading-screen.d.ts +36 -0
  82. package/dist/types/components/fw-player-controls.d.ts +23 -6
  83. package/dist/types/components/fw-player.d.ts +32 -1
  84. package/dist/types/components/fw-seek-bar.d.ts +31 -14
  85. package/dist/types/components/fw-settings-menu.d.ts +16 -1
  86. package/dist/types/components/fw-stats-panel.d.ts +4 -4
  87. package/dist/types/components/fw-stream-state-overlay.d.ts +20 -0
  88. package/dist/types/components/fw-subtitle-renderer.d.ts +33 -2
  89. package/dist/types/components/fw-thumbnail-overlay.d.ts +17 -0
  90. package/dist/types/components/fw-volume-control.d.ts +11 -4
  91. package/dist/types/components/shared/hitmarker-audio.d.ts +1 -0
  92. package/dist/types/constants/media-assets.d.ts +5 -0
  93. package/dist/types/controllers/player-controller-host.d.ts +14 -1
  94. package/dist/types/iife-entry.d.ts +4 -0
  95. package/dist/types/index.d.ts +4 -0
  96. package/package.json +2 -2
  97. package/src/components/fw-dev-mode-panel.ts +929 -228
  98. package/src/components/fw-dvd-logo.ts +233 -0
  99. package/src/components/fw-idle-screen.ts +680 -100
  100. package/src/components/fw-loading-screen.ts +540 -0
  101. package/src/components/fw-player-controls.ts +475 -175
  102. package/src/components/fw-player.ts +551 -60
  103. package/src/components/fw-seek-bar.ts +336 -143
  104. package/src/components/fw-settings-menu.ts +248 -85
  105. package/src/components/fw-stats-panel.ts +150 -77
  106. package/src/components/fw-stream-state-overlay.ts +331 -0
  107. package/src/components/fw-subtitle-renderer.ts +216 -28
  108. package/src/components/fw-thumbnail-overlay.ts +148 -0
  109. package/src/components/fw-volume-control.ts +166 -66
  110. package/src/components/shared/hitmarker-audio.ts +92 -0
  111. package/src/constants/media-assets.ts +7 -0
  112. package/src/controllers/player-controller-host.ts +52 -3
  113. package/src/define.ts +8 -0
  114. package/src/iife-entry.ts +4 -0
  115. package/src/index.ts +4 -0
  116. package/dist/fw-player.iife.js.map +0 -1
@@ -1,133 +1,856 @@
1
1
  import { __decorate } from '../node_modules/.pnpm/@rollup_plugin-typescript@12.3.0_rollup@4.57.1_tslib@2.8.1_typescript@5.9.3/node_modules/tslib/tslib.es6.js';
2
- import { css, LitElement, html, nothing } from 'lit';
2
+ import { css, LitElement, nothing, html } from 'lit';
3
3
  import { property, state, customElement } from 'lit/decorators.js';
4
4
  import { classMap } from 'lit/directives/class-map.js';
5
5
  import { sharedStyles } from '../styles/shared-styles.js';
6
6
  import { utilityStyles } from '../styles/utility-styles.js';
7
7
  import { closeIcon } from '../icons/index.js';
8
+ import { globalPlayerManager, QualityMonitor } from '@livepeer-frameworks/player-core';
8
9
 
10
+ const SOURCE_TYPE_LABELS = {
11
+ "html5/application/vnd.apple.mpegurl": "HLS",
12
+ "dash/video/mp4": "DASH",
13
+ "html5/video/mp4": "MP4",
14
+ "html5/video/webm": "WebM",
15
+ whep: "WHEP",
16
+ "mist/html": "Mist",
17
+ "mist/legacy": "Auto",
18
+ "ws/video/mp4": "MEWS",
19
+ };
9
20
  let FwDevModePanel = class FwDevModePanel extends LitElement {
10
21
  constructor() {
11
22
  super(...arguments);
12
23
  this.playbackMode = "auto";
13
24
  this._activeTab = "config";
14
- this._modes = ["auto", "low-latency", "quality"];
25
+ this._hoveredComboIndex = null;
26
+ this._tooltipAbove = false;
27
+ this._showDisabledPlayers = false;
28
+ this._playbackScore = 1;
29
+ this._qualityScore = 100;
30
+ this._stallCount = 0;
31
+ this._frameDropRate = 0;
32
+ this._videoStats = null;
33
+ this._playerStats = null;
34
+ this._qualityMonitor = null;
35
+ this._qualityMonitorVideo = null;
36
+ this._videoStatsInterval = null;
37
+ this._playerStatsInterval = null;
15
38
  }
16
- render() {
17
- const s = this.pc.s;
39
+ disconnectedCallback() {
40
+ super.disconnectedCallback();
41
+ this._stopQualityMonitor();
42
+ this._stopStatsPolling();
43
+ }
44
+ updated(_changed) {
45
+ this._syncQualityMonitor();
46
+ this._syncStatsPolling();
47
+ }
48
+ _getMistStreamInfo() {
49
+ return this.pc.s.streamState?.streamInfo;
50
+ }
51
+ _getAllCombinations() {
52
+ const streamInfo = this.pc.s.streamInfo;
53
+ if (!streamInfo) {
54
+ return [];
55
+ }
56
+ try {
57
+ return globalPlayerManager.getAllCombinations(streamInfo, this.playbackMode);
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ }
63
+ _getCompatibleCombinations() {
64
+ return this._getAllCombinations().filter((combo) => combo.compatible);
65
+ }
66
+ _getActiveComboIndex(combinations) {
67
+ const currentPlayer = this.pc.s.currentPlayerInfo;
68
+ const currentSource = this.pc.s.currentSourceInfo;
69
+ if (!currentPlayer || !currentSource || combinations.length === 0) {
70
+ return -1;
71
+ }
72
+ return combinations.findIndex((combo) => combo.player === currentPlayer.shortname && combo.sourceType === currentSource.type);
73
+ }
74
+ _syncQualityMonitor() {
75
+ const video = this.pc.s.videoElement;
76
+ if (!video) {
77
+ this._stopQualityMonitor();
78
+ return;
79
+ }
80
+ if (!this._qualityMonitor) {
81
+ this._qualityMonitor = new QualityMonitor({
82
+ sampleInterval: 500,
83
+ onSample: (quality) => {
84
+ this._qualityScore = quality.score;
85
+ this._stallCount = quality.stallCount;
86
+ this._frameDropRate = quality.frameDropRate;
87
+ this._playbackScore = this._qualityMonitor?.getPlaybackScore() ?? 1;
88
+ this.requestUpdate();
89
+ },
90
+ });
91
+ }
92
+ if (this._qualityMonitorVideo !== video) {
93
+ this._qualityMonitor.stop();
94
+ this._qualityMonitor.start(video);
95
+ this._qualityMonitorVideo = video;
96
+ }
97
+ }
98
+ _stopQualityMonitor() {
99
+ this._qualityMonitor?.stop();
100
+ this._qualityMonitorVideo = null;
101
+ }
102
+ _syncStatsPolling() {
103
+ if (this._activeTab !== "stats") {
104
+ this._stopStatsPolling();
105
+ return;
106
+ }
107
+ if (!this._videoStatsInterval) {
108
+ this._updateVideoStats();
109
+ this._videoStatsInterval = setInterval(() => {
110
+ this._updateVideoStats();
111
+ }, 500);
112
+ }
113
+ if (!this._playerStatsInterval) {
114
+ void this._pollPlayerStats();
115
+ this._playerStatsInterval = setInterval(() => {
116
+ void this._pollPlayerStats();
117
+ }, 500);
118
+ }
119
+ }
120
+ _stopStatsPolling() {
121
+ if (this._videoStatsInterval) {
122
+ clearInterval(this._videoStatsInterval);
123
+ this._videoStatsInterval = null;
124
+ }
125
+ if (this._playerStatsInterval) {
126
+ clearInterval(this._playerStatsInterval);
127
+ this._playerStatsInterval = null;
128
+ }
129
+ }
130
+ _updateVideoStats() {
131
+ const video = this.pc.s.videoElement;
132
+ if (!video) {
133
+ this._videoStats = null;
134
+ return;
135
+ }
136
+ this._videoStats = {
137
+ resolution: `${video.videoWidth}x${video.videoHeight}`,
138
+ buffered: video.buffered.length > 0
139
+ ? (video.buffered.end(video.buffered.length - 1) - video.currentTime).toFixed(1)
140
+ : "0",
141
+ playbackRate: video.playbackRate.toFixed(2),
142
+ currentTime: video.currentTime.toFixed(1),
143
+ duration: Number.isFinite(video.duration) ? video.duration.toFixed(1) : "live",
144
+ readyState: video.readyState,
145
+ networkState: video.networkState,
146
+ };
147
+ }
148
+ async _pollPlayerStats() {
149
+ try {
150
+ const stats = await this.pc.getStats();
151
+ if (stats) {
152
+ this._playerStats = stats;
153
+ }
154
+ }
155
+ catch {
156
+ // No-op for optional stats backends.
157
+ }
158
+ }
159
+ _handleComboMouseEnter(index, event) {
160
+ this._hoveredComboIndex = index;
161
+ const container = this.renderRoot.querySelector(".fw-dev-body");
162
+ if (!container) {
163
+ return;
164
+ }
165
+ const row = event.currentTarget;
166
+ const containerRect = container.getBoundingClientRect();
167
+ const rowRect = row.getBoundingClientRect();
168
+ const relativePosition = (rowRect.top - containerRect.top) / containerRect.height;
169
+ this._tooltipAbove = relativePosition > 0.6;
170
+ }
171
+ _handleModeChange(mode) {
172
+ this.playbackMode = mode;
173
+ void this.pc.setDevModeOptions({ playbackMode: mode });
174
+ this.dispatchEvent(new CustomEvent("fw-playback-mode-change", {
175
+ detail: { mode },
176
+ bubbles: true,
177
+ composed: true,
178
+ }));
179
+ }
180
+ _handleReload() {
181
+ this.pc.clearError();
182
+ void this.pc.reload();
183
+ }
184
+ _handleNextCombo() {
185
+ const compatible = this._getCompatibleCombinations();
186
+ if (compatible.length === 0) {
187
+ return;
188
+ }
189
+ const activeCompatibleIndex = this._getActiveComboIndex(compatible);
190
+ const startIndex = activeCompatibleIndex >= 0 ? activeCompatibleIndex : -1;
191
+ const nextIndex = (startIndex + 1) % compatible.length;
192
+ const next = compatible[nextIndex];
193
+ void this.pc.setDevModeOptions({
194
+ forcePlayer: next.player,
195
+ forceType: next.sourceType,
196
+ forceSource: next.sourceIndex,
197
+ });
198
+ }
199
+ _handleSelectCombo(index) {
200
+ const allCombinations = this._getAllCombinations();
201
+ const combo = allCombinations[index];
202
+ if (!combo) {
203
+ return;
204
+ }
205
+ void this.pc.setDevModeOptions({
206
+ forcePlayer: combo.player,
207
+ forceType: combo.sourceType,
208
+ forceSource: combo.sourceIndex,
209
+ });
210
+ }
211
+ _renderStatsTab() {
212
+ const primaryEndpoint = (this.pc.s.endpoints?.primary ?? null);
213
+ const stats = this._videoStats;
214
+ const playerStats = this._playerStats;
215
+ const mistStreamInfo = this._getMistStreamInfo();
216
+ const trackEntries = Object.entries(mistStreamInfo?.meta?.tracks ?? {});
18
217
  return html `
19
- <div class="panel fw-dev-panel">
20
- <div class="header fw-dev-header">
21
- <div class="tabs">
22
- <button
23
- class=${classMap({ tab: true, "tab--active": this._activeTab === "config" })}
24
- @click=${() => {
25
- this._activeTab = "config";
26
- }}
218
+ <div class="fw-dev-body">
219
+ <div class="fw-dev-section">
220
+ <div class="fw-dev-label">Playback Rate</div>
221
+ <div class="fw-dev-rate">
222
+ <div
223
+ class=${classMap({
224
+ "fw-dev-rate-value": true,
225
+ "fw-dev-stat-value--good": this._playbackScore >= 0.95 && this._playbackScore <= 1.05,
226
+ "fw-dev-stat-value--accent": this._playbackScore > 1.05,
227
+ "fw-dev-stat-value--warn": this._playbackScore >= 0.75 && this._playbackScore < 0.95,
228
+ "fw-dev-stat-value--bad": this._playbackScore < 0.75,
229
+ })}
27
230
  >
28
- Config
29
- </button>
30
- <button
31
- class=${classMap({ tab: true, "tab--active": this._activeTab === "stats" })}
32
- @click=${() => {
33
- this._activeTab = "stats";
34
- }}
231
+ ${this._playbackScore.toFixed(2)}x
232
+ </div>
233
+ <div class="fw-dev-rate-status">
234
+ ${this._playbackScore >= 0.95 && this._playbackScore <= 1.05
235
+ ? "realtime"
236
+ : this._playbackScore > 1.05
237
+ ? "catching up"
238
+ : this._playbackScore >= 0.75
239
+ ? "slightly slow"
240
+ : "stalling"}
241
+ </div>
242
+ </div>
243
+ <div class="fw-dev-rate-stats">
244
+ <span
245
+ class=${classMap({
246
+ "fw-dev-stat-value--good": this._qualityScore >= 75,
247
+ "fw-dev-stat-value--bad": this._qualityScore < 75,
248
+ })}
249
+ >
250
+ Quality: ${this._qualityScore}/100
251
+ </span>
252
+ <span
253
+ class=${classMap({
254
+ "fw-dev-stat-value--good": this._stallCount === 0,
255
+ "fw-dev-stat-value--warn": this._stallCount > 0,
256
+ })}
257
+ >
258
+ Stalls: ${this._stallCount}
259
+ </span>
260
+ <span
261
+ class=${classMap({
262
+ "fw-dev-stat-value--good": this._frameDropRate < 1,
263
+ "fw-dev-stat-value--bad": this._frameDropRate >= 1,
264
+ })}
35
265
  >
36
- Stats
37
- </button>
266
+ Drops: ${this._frameDropRate.toFixed(1)}%
267
+ </span>
38
268
  </div>
39
- <button
40
- class="close"
41
- @click=${() => this.dispatchEvent(new CustomEvent("fw-close", { bubbles: true, composed: true }))}
42
- aria-label="Close panel"
43
- >
44
- ${closeIcon()}
45
- </button>
46
269
  </div>
47
270
 
48
- <div class="body fw-dev-body">
49
- ${this._activeTab === "config" ? this._renderConfig(s) : this._renderStats(s)}
50
- </div>
271
+ ${stats
272
+ ? html `
273
+ <div class="fw-dev-stat">
274
+ <span class="fw-dev-stat-label">Resolution</span>
275
+ <span class="fw-dev-stat-value">${stats.resolution}</span>
276
+ </div>
277
+ <div class="fw-dev-stat">
278
+ <span class="fw-dev-stat-label">Buffer</span>
279
+ <span class="fw-dev-stat-value">${stats.buffered}s</span>
280
+ </div>
281
+ <div class="fw-dev-stat">
282
+ <span class="fw-dev-stat-label">Playback Rate</span>
283
+ <span class="fw-dev-stat-value">${stats.playbackRate}x</span>
284
+ </div>
285
+ <div class="fw-dev-stat">
286
+ <span class="fw-dev-stat-label">Time</span>
287
+ <span class="fw-dev-stat-value">${stats.currentTime} / ${stats.duration}</span>
288
+ </div>
289
+ <div class="fw-dev-stat">
290
+ <span class="fw-dev-stat-label">Ready State</span>
291
+ <span class="fw-dev-stat-value">${stats.readyState}</span>
292
+ </div>
293
+ <div class="fw-dev-stat">
294
+ <span class="fw-dev-stat-label">Network State</span>
295
+ <span class="fw-dev-stat-value">${stats.networkState}</span>
296
+ </div>
297
+ ${primaryEndpoint?.protocol
298
+ ? html `
299
+ <div class="fw-dev-stat">
300
+ <span class="fw-dev-stat-label">Protocol</span>
301
+ <span class="fw-dev-stat-value">${primaryEndpoint.protocol}</span>
302
+ </div>
303
+ `
304
+ : nothing}
305
+ ${primaryEndpoint?.nodeId
306
+ ? html `
307
+ <div class="fw-dev-stat">
308
+ <span class="fw-dev-stat-label">Node ID</span>
309
+ <span
310
+ class="fw-dev-stat-value"
311
+ style="max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"
312
+ >${primaryEndpoint.nodeId}</span
313
+ >
314
+ </div>
315
+ `
316
+ : nothing}
317
+ `
318
+ : html `<div class="fw-dev-list-empty">No video element available</div>`}
319
+ ${playerStats
320
+ ? html `
321
+ <div class="fw-dev-list-header fw-dev-section-header">
322
+ <span class="fw-dev-list-title"
323
+ >${playerStats.type === "hls"
324
+ ? "HLS.js Stats"
325
+ : playerStats.type === "webrtc"
326
+ ? "WebRTC Stats"
327
+ : "Player Stats"}</span
328
+ >
329
+ </div>
330
+
331
+ ${playerStats.type === "hls"
332
+ ? html `
333
+ <div class="fw-dev-stat">
334
+ <span class="fw-dev-stat-label">Bitrate</span>
335
+ <span class="fw-dev-stat-value--accent"
336
+ >${typeof playerStats.currentBitrate === "number" &&
337
+ playerStats.currentBitrate > 0
338
+ ? `${Math.round(playerStats.currentBitrate / 1000)} kbps`
339
+ : "N/A"}</span
340
+ >
341
+ </div>
342
+ <div class="fw-dev-stat">
343
+ <span class="fw-dev-stat-label">Bandwidth Est.</span>
344
+ <span class="fw-dev-stat-value"
345
+ >${typeof playerStats.bandwidthEstimate === "number" &&
346
+ playerStats.bandwidthEstimate > 0
347
+ ? `${Math.round(playerStats.bandwidthEstimate / 1000)} kbps`
348
+ : "N/A"}</span
349
+ >
350
+ </div>
351
+ <div class="fw-dev-stat">
352
+ <span class="fw-dev-stat-label">Level</span>
353
+ <span class="fw-dev-stat-value"
354
+ >${typeof playerStats.currentLevel === "number" &&
355
+ playerStats.currentLevel >= 0
356
+ ? playerStats.currentLevel
357
+ : "Auto"}
358
+ / ${Array.isArray(playerStats.levels) ? playerStats.levels.length : 0}</span
359
+ >
360
+ </div>
361
+ ${typeof playerStats.latency === "number"
362
+ ? html `
363
+ <div class="fw-dev-stat">
364
+ <span class="fw-dev-stat-label">Latency</span>
365
+ <span
366
+ class=${classMap({
367
+ "fw-dev-stat-value": true,
368
+ "fw-dev-stat-value--warn": playerStats.latency > 5000,
369
+ })}
370
+ >${Math.round(playerStats.latency)} ms</span
371
+ >
372
+ </div>
373
+ `
374
+ : nothing}
375
+ `
376
+ : nothing}
377
+ ${playerStats.type === "webrtc"
378
+ ? html `
379
+ ${playerStats.video
380
+ ? html `
381
+ <div class="fw-dev-stat">
382
+ <span class="fw-dev-stat-label">Video Bitrate</span>
383
+ <span class="fw-dev-stat-value--accent"
384
+ >${typeof playerStats.video.bitrate === "number" &&
385
+ playerStats.video.bitrate > 0
386
+ ? `${Math.round(playerStats.video.bitrate / 1000)} kbps`
387
+ : "N/A"}</span
388
+ >
389
+ </div>
390
+ <div class="fw-dev-stat">
391
+ <span class="fw-dev-stat-label">FPS</span>
392
+ <span class="fw-dev-stat-value"
393
+ >${Math.round(playerStats.video.framesPerSecond || 0)}</span
394
+ >
395
+ </div>
396
+ <div class="fw-dev-stat">
397
+ <span class="fw-dev-stat-label">Frames</span>
398
+ <span class="fw-dev-stat-value"
399
+ >${playerStats.video.framesDecoded} decoded,
400
+ <span
401
+ class=${classMap({
402
+ "fw-dev-stat-value--bad": (playerStats.video.frameDropRate || 0) > 1,
403
+ "fw-dev-stat-value--good": (playerStats.video.frameDropRate || 0) <= 1,
404
+ })}
405
+ >${playerStats.video.framesDropped} dropped</span
406
+ ></span
407
+ >
408
+ </div>
409
+ <div class="fw-dev-stat">
410
+ <span class="fw-dev-stat-label">Packet Loss</span>
411
+ <span
412
+ class=${classMap({
413
+ "fw-dev-stat-value--bad": (playerStats.video.packetLossRate || 0) > 1,
414
+ "fw-dev-stat-value--good": (playerStats.video.packetLossRate || 0) <= 1,
415
+ })}
416
+ >${(playerStats.video.packetLossRate || 0).toFixed(2)}%</span
417
+ >
418
+ </div>
419
+ <div class="fw-dev-stat">
420
+ <span class="fw-dev-stat-label">Jitter</span>
421
+ <span
422
+ class=${classMap({
423
+ "fw-dev-stat-value": true,
424
+ "fw-dev-stat-value--warn": (playerStats.video.jitter || 0) > 30,
425
+ })}
426
+ >${(playerStats.video.jitter || 0).toFixed(1)}
427
+ ms</span
428
+ >
429
+ </div>
430
+ <div class="fw-dev-stat">
431
+ <span class="fw-dev-stat-label">Jitter Buffer</span>
432
+ <span class="fw-dev-stat-value"
433
+ >${(playerStats.video.jitterBufferDelay || 0).toFixed(1)}
434
+ ms</span
435
+ >
436
+ </div>
437
+ `
438
+ : nothing}
439
+ ${playerStats.network
440
+ ? html `
441
+ <div class="fw-dev-stat">
442
+ <span class="fw-dev-stat-label">RTT</span>
443
+ <span
444
+ class=${classMap({
445
+ "fw-dev-stat-value": true,
446
+ "fw-dev-stat-value--warn": (playerStats.network.rtt || 0) > 200,
447
+ })}
448
+ >${Math.round((playerStats.network.rtt || 0))}
449
+ ms</span
450
+ >
451
+ </div>
452
+ `
453
+ : nothing}
454
+ `
455
+ : nothing}
456
+ `
457
+ : nothing}
458
+ ${trackEntries.length > 0
459
+ ? html `
460
+ <div class="fw-dev-list-header fw-dev-section-header">
461
+ <span class="fw-dev-list-title">Tracks (${trackEntries.length})</span>
462
+ </div>
463
+ ${trackEntries.map(([id, track]) => {
464
+ const typedTrack = track;
465
+ return html `
466
+ <div class="fw-dev-track">
467
+ <div class="fw-dev-track-header">
468
+ <span
469
+ class=${classMap({
470
+ "fw-dev-track-badge": true,
471
+ "fw-dev-track-badge--video": typedTrack.type === "video",
472
+ "fw-dev-track-badge--audio": typedTrack.type === "audio",
473
+ "fw-dev-track-badge--other": typedTrack.type !== "video" && typedTrack.type !== "audio",
474
+ })}
475
+ >${typedTrack.type}</span
476
+ >
477
+ <span class="fw-dev-track-codec">${typedTrack.codec}</span>
478
+ <span class="fw-dev-track-id">#${id}</span>
479
+ </div>
480
+ <div class="fw-dev-track-meta">
481
+ ${typedTrack.type === "video" && typedTrack.width && typedTrack.height
482
+ ? html `<span>${typedTrack.width}x${typedTrack.height}</span>`
483
+ : nothing}
484
+ ${typedTrack.bps
485
+ ? html `<span>${Math.round(typedTrack.bps / 1000)} kbps</span>`
486
+ : nothing}
487
+ ${typedTrack.fpks
488
+ ? html `<span>${Math.round(typedTrack.fpks / 1000)} fps</span>`
489
+ : nothing}
490
+ ${typedTrack.type === "audio" && typedTrack.channels
491
+ ? html `<span>${typedTrack.channels}ch</span>`
492
+ : nothing}
493
+ ${typedTrack.type === "audio" && typedTrack.rate
494
+ ? html `<span>${typedTrack.rate} Hz</span>`
495
+ : nothing}
496
+ ${typedTrack.lang ? html `<span>${typedTrack.lang}</span>` : nothing}
497
+ </div>
498
+ </div>
499
+ `;
500
+ })}
501
+ `
502
+ : nothing}
503
+ ${mistStreamInfo && trackEntries.length === 0
504
+ ? html `
505
+ <div class="fw-dev-no-tracks">
506
+ <span class="fw-dev-no-tracks-text"
507
+ >No track data available
508
+ ${mistStreamInfo.type
509
+ ? html `<span class="fw-dev-no-tracks-type">(${mistStreamInfo.type})</span>`
510
+ : nothing}</span
511
+ >
512
+ </div>
513
+ `
514
+ : nothing}
51
515
  </div>
52
516
  `;
53
517
  }
54
- _renderConfig(s) {
518
+ _renderConfigTab() {
519
+ const allCombinations = this._getAllCombinations();
520
+ const compatibleCombinations = allCombinations.filter((combo) => combo.compatible);
521
+ const activeComboIndex = this._getActiveComboIndex(allCombinations);
522
+ const currentPlayer = this.pc.s.currentPlayerInfo;
523
+ const currentSource = this.pc.s.currentSourceInfo;
55
524
  return html `
56
- <div class="section">
57
- <div class="label">Current Player</div>
58
- <div class="value">${s.currentPlayerInfo?.name ?? "—"}</div>
59
- </div>
60
- <div class="section">
61
- <div class="label">Current Source</div>
62
- <div class="value">${s.currentSourceInfo?.type ?? ""}</div>
63
- </div>
64
- <div class="section">
65
- <div class="label">Playback Mode</div>
66
- <div class="mode-group fw-dev-mode-group">
67
- ${this._modes.map((mode) => html `
68
- <button
69
- class=${classMap({
70
- "mode-btn": true,
525
+ <div class="fw-dev-body">
526
+ <div class="fw-dev-section">
527
+ <div class="fw-dev-label">Active</div>
528
+ <div class="fw-dev-value">
529
+ ${currentPlayer?.name || "None"}
530
+ <span class="fw-dev-value-arrow">${"\u2192"}</span>
531
+ ${SOURCE_TYPE_LABELS[currentSource?.type || ""] || currentSource?.type || "-"}
532
+ </div>
533
+ ${this.pc.s.endpoints?.primary?.nodeId
534
+ ? html `
535
+ <div class="fw-dev-value-muted">
536
+ Node: ${(this.pc.s.endpoints?.primary).nodeId}
537
+ </div>
538
+ `
539
+ : nothing}
540
+ </div>
541
+
542
+ <div class="fw-dev-section">
543
+ <div class="fw-dev-label">Playback Mode</div>
544
+ <div class="fw-dev-mode-group">
545
+ ${["auto", "low-latency", "quality"].map((mode) => html `
546
+ <button
547
+ type="button"
548
+ class=${classMap({
71
549
  "fw-dev-mode-btn": true,
72
- "mode-btn--active": this.playbackMode === mode,
73
550
  "fw-dev-mode-btn--active": this.playbackMode === mode,
74
551
  })}
75
- @click=${() => this.pc.setDevModeOptions({ playbackMode: mode })}
76
- >
77
- ${mode}
78
- </button>
79
- `)}
552
+ @click=${() => this._handleModeChange(mode)}
553
+ >
554
+ ${mode === "low-latency"
555
+ ? "Low Lat"
556
+ : `${mode.charAt(0).toUpperCase()}${mode.slice(1)}`}
557
+ </button>
558
+ `)}
559
+ </div>
560
+ <div class="fw-dev-mode-desc">
561
+ ${this.playbackMode === "auto"
562
+ ? "Balanced: MP4/WS \u2192 WHEP \u2192 HLS"
563
+ : this.playbackMode === "low-latency"
564
+ ? "WHEP/WebRTC first (<1s delay)"
565
+ : "MP4/WS first, HLS fallback"}
566
+ </div>
567
+ </div>
568
+
569
+ <div class="fw-dev-actions">
570
+ <button type="button" class="fw-dev-action-btn" @click=${this._handleReload}>
571
+ Reload
572
+ </button>
573
+ <button type="button" class="fw-dev-action-btn" @click=${this._handleNextCombo}>
574
+ Next Option
575
+ </button>
576
+ </div>
577
+
578
+ <div class="fw-dev-section" style="padding:0;border-bottom:0;">
579
+ <div class="fw-dev-list-header">
580
+ <span class="fw-dev-list-title">Player Options (${compatibleCombinations.length})</span>
581
+ ${allCombinations.length > compatibleCombinations.length
582
+ ? html `
583
+ <button
584
+ type="button"
585
+ class="fw-dev-list-toggle"
586
+ @click=${() => {
587
+ this._showDisabledPlayers = !this._showDisabledPlayers;
588
+ }}
589
+ >
590
+ <svg
591
+ width="10"
592
+ height="10"
593
+ viewBox="0 0 24 24"
594
+ fill="none"
595
+ stroke="currentColor"
596
+ stroke-width="2"
597
+ class=${classMap({
598
+ "fw-dev-chevron": true,
599
+ "fw-dev-chevron--open": this._showDisabledPlayers,
600
+ })}
601
+ >
602
+ <path d="M6 9l6 6 6-6"></path>
603
+ </svg>
604
+ ${this._showDisabledPlayers ? "Hide" : "Show"} disabled
605
+ (${allCombinations.length - compatibleCombinations.length})
606
+ </button>
607
+ `
608
+ : nothing}
609
+ </div>
610
+
611
+ ${allCombinations.length === 0
612
+ ? html `<div class="fw-dev-list-empty">No stream info available</div>`
613
+ : html `
614
+ ${allCombinations.map((combo, index) => {
615
+ const isCodecIncompatible = combo.codecIncompatible === true;
616
+ if (!combo.compatible && !isCodecIncompatible && !this._showDisabledPlayers) {
617
+ return nothing;
618
+ }
619
+ const isActive = activeComboIndex === index;
620
+ const typeLabel = SOURCE_TYPE_LABELS[combo.sourceType] || combo.sourceType.split("/").pop();
621
+ const scoreClass = !combo.compatible && !isCodecIncompatible
622
+ ? "fw-dev-combo-score--disabled"
623
+ : isCodecIncompatible
624
+ ? "fw-dev-combo-score--low"
625
+ : combo.score >= 2
626
+ ? "fw-dev-combo-score--high"
627
+ : combo.score >= 1.5
628
+ ? "fw-dev-combo-score--mid"
629
+ : "fw-dev-combo-score--low";
630
+ const rankClass = isActive
631
+ ? "fw-dev-combo-rank--active"
632
+ : !combo.compatible && !isCodecIncompatible
633
+ ? "fw-dev-combo-rank--disabled"
634
+ : isCodecIncompatible
635
+ ? "fw-dev-combo-rank--warn"
636
+ : "";
637
+ const typeClass = !combo.compatible && !isCodecIncompatible
638
+ ? "fw-dev-combo-type--disabled"
639
+ : isCodecIncompatible
640
+ ? "fw-dev-combo-type--warn"
641
+ : "";
642
+ return html `
643
+ <div
644
+ class="fw-dev-combo"
645
+ @mouseenter=${(event) => this._handleComboMouseEnter(index, event)}
646
+ @mouseleave=${() => {
647
+ this._hoveredComboIndex = null;
648
+ }}
649
+ >
650
+ <button
651
+ type="button"
652
+ class=${classMap({
653
+ "fw-dev-combo-btn": true,
654
+ "fw-dev-combo-btn--active": isActive,
655
+ "fw-dev-combo-btn--disabled": !combo.compatible && !isCodecIncompatible,
656
+ "fw-dev-combo-btn--codec-warn": isCodecIncompatible,
657
+ })}
658
+ @click=${() => this._handleSelectCombo(index)}
659
+ >
660
+ <span
661
+ class=${classMap({
662
+ "fw-dev-combo-rank": true,
663
+ [rankClass]: rankClass.length > 0,
664
+ })}
665
+ >${combo.compatible
666
+ ? index + 1
667
+ : isCodecIncompatible
668
+ ? "\u26A0"
669
+ : "\u2014"}</span
670
+ >
671
+
672
+ <span class="fw-dev-combo-name"
673
+ >${combo.playerName} <span class="fw-dev-combo-arrow">${"\u2192"}</span>
674
+ <span
675
+ class=${classMap({
676
+ "fw-dev-combo-type": true,
677
+ [typeClass]: typeClass.length > 0,
678
+ })}
679
+ >${typeLabel}</span
680
+ ></span
681
+ >
682
+
683
+ <span class=${classMap({ "fw-dev-combo-score": true, [scoreClass]: true })}
684
+ >${combo.score.toFixed(2)}</span
685
+ >
686
+ </button>
687
+
688
+ ${this._hoveredComboIndex === index
689
+ ? html `
690
+ <div
691
+ class=${classMap({
692
+ "fw-dev-tooltip": true,
693
+ "fw-dev-tooltip--above": this._tooltipAbove,
694
+ "fw-dev-tooltip--below": !this._tooltipAbove,
695
+ })}
696
+ >
697
+ <div class="fw-dev-tooltip-header">
698
+ <div class="fw-dev-tooltip-title">${combo.playerName}</div>
699
+ <div class="fw-dev-tooltip-subtitle">${combo.sourceType}</div>
700
+ ${combo.scoreBreakdown?.trackTypes &&
701
+ combo.scoreBreakdown.trackTypes.length > 0
702
+ ? html `
703
+ <div class="fw-dev-tooltip-tracks">
704
+ Tracks:
705
+ <span class="fw-dev-tooltip-value"
706
+ >${combo.scoreBreakdown.trackTypes.join(", ")}</span
707
+ >
708
+ </div>
709
+ `
710
+ : nothing}
711
+ </div>
712
+
713
+ ${combo.compatible && combo.scoreBreakdown
714
+ ? html `
715
+ <div class="fw-dev-tooltip-score">
716
+ Score: ${combo.score.toFixed(2)}
717
+ </div>
718
+ <div class="fw-dev-tooltip-row">
719
+ Tracks [${combo.scoreBreakdown.trackTypes.join(", ")}]:
720
+ <span class="fw-dev-tooltip-value"
721
+ >${combo.scoreBreakdown.trackScore.toFixed(2)}</span
722
+ >
723
+ <span class="fw-dev-tooltip-weight"
724
+ >x${combo.scoreBreakdown.weights.tracks}</span
725
+ >
726
+ </div>
727
+ <div class="fw-dev-tooltip-row">
728
+ Priority:
729
+ <span class="fw-dev-tooltip-value"
730
+ >${combo.scoreBreakdown.priorityScore.toFixed(2)}</span
731
+ >
732
+ <span class="fw-dev-tooltip-weight"
733
+ >x${combo.scoreBreakdown.weights.priority}</span
734
+ >
735
+ </div>
736
+ <div class="fw-dev-tooltip-row">
737
+ Source:
738
+ <span class="fw-dev-tooltip-value"
739
+ >${combo.scoreBreakdown.sourceScore.toFixed(2)}</span
740
+ >
741
+ <span class="fw-dev-tooltip-weight"
742
+ >x${combo.scoreBreakdown.weights.source}</span
743
+ >
744
+ </div>
745
+
746
+ ${typeof combo.scoreBreakdown.reliabilityScore === "number"
747
+ ? html `
748
+ <div class="fw-dev-tooltip-row">
749
+ Reliability:
750
+ <span class="fw-dev-tooltip-value"
751
+ >${combo.scoreBreakdown.reliabilityScore.toFixed(2)}</span
752
+ >
753
+ <span class="fw-dev-tooltip-weight"
754
+ >x${combo.scoreBreakdown.weights.reliability ??
755
+ 0}</span
756
+ >
757
+ </div>
758
+ `
759
+ : nothing}
760
+ ${typeof combo.scoreBreakdown.modeBonus === "number" &&
761
+ combo.scoreBreakdown.modeBonus !== 0
762
+ ? html `
763
+ <div class="fw-dev-tooltip-row">
764
+ Mode (${this.playbackMode}):
765
+ <span class="fw-dev-tooltip-bonus"
766
+ >+${combo.scoreBreakdown.modeBonus.toFixed(2)}</span
767
+ >
768
+ <span class="fw-dev-tooltip-weight"
769
+ >x${combo.scoreBreakdown.weights.mode ?? 0}</span
770
+ >
771
+ </div>
772
+ `
773
+ : nothing}
774
+ ${typeof combo.scoreBreakdown.routingBonus === "number" &&
775
+ combo.scoreBreakdown.routingBonus !== 0
776
+ ? html `
777
+ <div class="fw-dev-tooltip-row">
778
+ Routing:
779
+ <span
780
+ class=${classMap({
781
+ "fw-dev-tooltip-bonus": combo.scoreBreakdown.routingBonus > 0,
782
+ "fw-dev-tooltip-penalty": combo.scoreBreakdown.routingBonus < 0,
783
+ })}
784
+ >${combo.scoreBreakdown.routingBonus > 0
785
+ ? "+"
786
+ : ""}${combo.scoreBreakdown.routingBonus.toFixed(2)}</span
787
+ >
788
+ <span class="fw-dev-tooltip-weight"
789
+ >x${combo.scoreBreakdown.weights.routing ?? 0}</span
790
+ >
791
+ </div>
792
+ `
793
+ : nothing}
794
+ `
795
+ : html `
796
+ <div class="fw-dev-tooltip-error">
797
+ ${combo.incompatibleReason || "Incompatible"}
798
+ </div>
799
+ `}
800
+ </div>
801
+ `
802
+ : nothing}
803
+ </div>
804
+ `;
805
+ })}
806
+ `}
80
807
  </div>
81
- </div>
82
- <div class="actions fw-dev-actions">
83
- <button
84
- class="action-btn fw-dev-action-btn"
85
- @click=${() => {
86
- this.pc.clearError();
87
- this.pc.reload();
88
- }}
89
- >
90
- Reload
91
- </button>
92
808
  </div>
93
809
  `;
94
810
  }
95
- _renderStats(s) {
96
- const q = s.playbackQuality;
811
+ render() {
97
812
  return html `
98
- <div class="section">
99
- <div class="label">Playback</div>
100
- ${this._row("State", s.state)}
101
- ${this._row("Time", `${s.currentTime.toFixed(1)}s / ${isFinite(s.duration) ? s.duration.toFixed(1) + "s" : "∞"}`)}
102
- ${this._row("Volume", `${Math.round(s.volume * 100)}%${s.isMuted ? " (muted)" : ""}`)}
813
+ <div class="fw-dev-panel">
814
+ <div class="fw-dev-header">
815
+ <button
816
+ type="button"
817
+ class=${classMap({
818
+ "fw-dev-tab": true,
819
+ "fw-dev-tab--active": this._activeTab === "config",
820
+ })}
821
+ @click=${() => {
822
+ this._activeTab = "config";
823
+ }}
824
+ >
825
+ Config
826
+ </button>
827
+ <button
828
+ type="button"
829
+ class=${classMap({
830
+ "fw-dev-tab": true,
831
+ "fw-dev-tab--active": this._activeTab === "stats",
832
+ })}
833
+ @click=${() => {
834
+ this._activeTab = "stats";
835
+ }}
836
+ >
837
+ Stats
838
+ </button>
839
+ <div class="fw-dev-spacer"></div>
840
+ <button
841
+ type="button"
842
+ class="fw-dev-close"
843
+ aria-label="Close dev mode panel"
844
+ @click=${() => this.dispatchEvent(new CustomEvent("fw-close", { bubbles: true, composed: true }))}
845
+ >
846
+ ${closeIcon()}
847
+ </button>
848
+ </div>
849
+
850
+ ${this._activeTab === "config" ? this._renderConfigTab() : this._renderStatsTab()}
103
851
  </div>
104
- ${q
105
- ? html `
106
- <div class="section">
107
- <div class="label">Quality</div>
108
- ${this._row("Resolution", this._resolution())}
109
- ${this._row("Bitrate", q.bitrate ? `${Math.round(q.bitrate / 1000)} kbps` : "—")}
110
- ${this._row("Latency", q.latency != null ? `${q.latency.toFixed(2)}s` : "—")}
111
- ${this._row("Buffer", q.bufferedAhead != null ? `${q.bufferedAhead.toFixed(1)}s` : "—")}
112
- ${this._row("Score", q.score != null ? `${q.score.toFixed(0)}` : "—")}
113
- ${this._row("Drops", `${q.frameDropRate?.toFixed(1) ?? "0"}%`)}
114
- ${this._row("Stalls", `${q.stallCount ?? 0}`)}
115
- </div>
116
- `
117
- : nothing}
118
852
  `;
119
853
  }
120
- _resolution() {
121
- const video = this.pc.s.videoElement;
122
- if (!video || !video.videoWidth || !video.videoHeight)
123
- return "—";
124
- return `${video.videoWidth}×${video.videoHeight}`;
125
- }
126
- _row(label, value) {
127
- return html `<div class="stat-row">
128
- <span class="stat-label">${label}</span><span class="stat-value">${value}</span>
129
- </div>`;
130
- }
131
854
  };
132
855
  FwDevModePanel.styles = [
133
856
  sharedStyles,
@@ -135,124 +858,7 @@ FwDevModePanel.styles = [
135
858
  css `
136
859
  :host {
137
860
  display: block;
138
- }
139
- .panel {
140
- width: 320px;
141
861
  height: 100%;
142
- border-left: 1px solid rgb(255 255 255 / 0.1);
143
- background: rgb(15 23 42);
144
- overflow: auto;
145
- font-size: 0.75rem;
146
- color: rgb(255 255 255 / 0.7);
147
- }
148
- .header {
149
- display: flex;
150
- align-items: center;
151
- justify-content: space-between;
152
- padding: 0.5rem 0.75rem;
153
- border-bottom: 1px solid rgb(255 255 255 / 0.1);
154
- }
155
- .tabs {
156
- display: flex;
157
- gap: 0.5rem;
158
- }
159
- .tab {
160
- padding: 0.25rem 0.5rem;
161
- border: none;
162
- background: none;
163
- color: rgb(255 255 255 / 0.5);
164
- font-size: 0.6875rem;
165
- font-weight: 600;
166
- cursor: pointer;
167
- border-radius: 0.25rem;
168
- }
169
- .tab--active {
170
- color: white;
171
- background: rgb(255 255 255 / 0.1);
172
- }
173
- .close {
174
- display: flex;
175
- background: none;
176
- border: none;
177
- color: rgb(255 255 255 / 0.5);
178
- cursor: pointer;
179
- padding: 0;
180
- }
181
- .close:hover {
182
- color: white;
183
- }
184
- .body {
185
- padding: 0.75rem;
186
- }
187
- .section {
188
- margin-bottom: 0.75rem;
189
- }
190
- .label {
191
- font-size: 0.625rem;
192
- font-weight: 600;
193
- text-transform: uppercase;
194
- letter-spacing: 0.05em;
195
- color: rgb(255 255 255 / 0.4);
196
- margin-bottom: 0.375rem;
197
- }
198
- .value {
199
- color: rgb(255 255 255 / 0.9);
200
- font-family: ui-monospace, monospace;
201
- }
202
- .mode-group {
203
- display: flex;
204
- gap: 0.25rem;
205
- flex-wrap: wrap;
206
- }
207
- .mode-btn {
208
- padding: 0.25rem 0.5rem;
209
- border: 1px solid rgb(255 255 255 / 0.15);
210
- background: none;
211
- color: rgb(255 255 255 / 0.6);
212
- font-size: 0.6875rem;
213
- cursor: pointer;
214
- border-radius: 0.25rem;
215
- transition: all 150ms;
216
- }
217
- .mode-btn:hover {
218
- border-color: rgb(255 255 255 / 0.3);
219
- color: white;
220
- }
221
- .mode-btn--active {
222
- border-color: hsl(var(--tn-blue, 217 89% 61%));
223
- color: hsl(var(--tn-blue, 217 89% 61%));
224
- background: hsl(var(--tn-blue, 217 89% 61%) / 0.1);
225
- }
226
- .actions {
227
- display: flex;
228
- gap: 0.5rem;
229
- margin-top: 0.5rem;
230
- }
231
- .action-btn {
232
- padding: 0.375rem 0.75rem;
233
- border: 1px solid rgb(255 255 255 / 0.15);
234
- background: none;
235
- color: rgb(255 255 255 / 0.7);
236
- font-size: 0.6875rem;
237
- cursor: pointer;
238
- border-radius: 0.25rem;
239
- }
240
- .action-btn:hover {
241
- border-color: rgb(255 255 255 / 0.3);
242
- color: white;
243
- }
244
- .stat-row {
245
- display: flex;
246
- justify-content: space-between;
247
- padding: 0.125rem 0;
248
- }
249
- .stat-label {
250
- color: rgb(255 255 255 / 0.4);
251
- }
252
- .stat-value {
253
- color: rgb(255 255 255 / 0.8);
254
- font-family: ui-monospace, monospace;
255
- font-variant-numeric: tabular-nums;
256
862
  }
257
863
  `,
258
864
  ];
@@ -265,6 +871,33 @@ __decorate([
265
871
  __decorate([
266
872
  state()
267
873
  ], FwDevModePanel.prototype, "_activeTab", void 0);
874
+ __decorate([
875
+ state()
876
+ ], FwDevModePanel.prototype, "_hoveredComboIndex", void 0);
877
+ __decorate([
878
+ state()
879
+ ], FwDevModePanel.prototype, "_tooltipAbove", void 0);
880
+ __decorate([
881
+ state()
882
+ ], FwDevModePanel.prototype, "_showDisabledPlayers", void 0);
883
+ __decorate([
884
+ state()
885
+ ], FwDevModePanel.prototype, "_playbackScore", void 0);
886
+ __decorate([
887
+ state()
888
+ ], FwDevModePanel.prototype, "_qualityScore", void 0);
889
+ __decorate([
890
+ state()
891
+ ], FwDevModePanel.prototype, "_stallCount", void 0);
892
+ __decorate([
893
+ state()
894
+ ], FwDevModePanel.prototype, "_frameDropRate", void 0);
895
+ __decorate([
896
+ state()
897
+ ], FwDevModePanel.prototype, "_videoStats", void 0);
898
+ __decorate([
899
+ state()
900
+ ], FwDevModePanel.prototype, "_playerStats", void 0);
268
901
  FwDevModePanel = __decorate([
269
902
  customElement("fw-dev-mode-panel")
270
903
  ], FwDevModePanel);