@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,17 +1,30 @@
1
1
  /**
2
- * <fw-settings-menu> — Quality, speed, and captions settings popup.
2
+ * <fw-settings-menu> — Mode, speed, quality, and captions settings popup.
3
3
  */
4
4
  import { LitElement, html, css, nothing } from "lit";
5
5
  import { customElement, property, state } from "lit/decorators.js";
6
6
  import { classMap } from "lit/directives/class-map.js";
7
7
  import { sharedStyles } from "../styles/shared-styles.js";
8
8
  import { utilityStyles } from "../styles/utility-styles.js";
9
+ import {
10
+ SPEED_PRESETS,
11
+ supportsPlaybackRate as coreSupportsPlaybackRate,
12
+ } from "@livepeer-frameworks/player-core";
13
+ import type { PlaybackMode } from "@livepeer-frameworks/player-core";
9
14
  import type { PlayerControllerHost } from "../controllers/player-controller-host.js";
10
15
 
11
16
  @customElement("fw-settings-menu")
12
17
  export class FwSettingsMenu extends LitElement {
13
18
  @property({ attribute: false }) pc!: PlayerControllerHost;
14
19
  @property({ type: Boolean }) open = false;
20
+ @property({ type: String }) playbackMode: PlaybackMode = "auto";
21
+ @property({ type: Boolean, attribute: "is-content-live" }) isContentLive = true;
22
+ @property({ type: Number, attribute: "playback-rate" }) playbackRate?: number;
23
+ @property({ type: String, attribute: "quality-value" }) qualityValue?: string;
24
+ @property({ type: String, attribute: "caption-value" }) captionValue?: string;
25
+ @property({ type: Boolean, attribute: "supports-playback-rate" }) supportsPlaybackRate?: boolean;
26
+
27
+ @state() private _playbackRate = 1;
15
28
 
16
29
  static styles = [
17
30
  sharedStyles,
@@ -20,102 +33,252 @@ export class FwSettingsMenu extends LitElement {
20
33
  :host {
21
34
  display: contents;
22
35
  }
23
- .menu {
24
- position: absolute;
25
- bottom: 100%;
26
- right: 0;
27
- margin-bottom: 0.5rem;
28
- min-width: 200px;
29
- border-radius: 0.5rem;
30
- border: 1px solid rgb(255 255 255 / 0.1);
31
- background: rgb(0 0 0 / 0.9);
32
- backdrop-filter: blur(8px);
33
- padding: 0.5rem;
34
- z-index: 50;
35
- box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.3);
36
- }
37
- .section {
38
- padding: 0.25rem 0;
39
- }
40
- .section + .section {
41
- border-top: 1px solid rgb(255 255 255 / 0.1);
42
- }
43
- .label {
44
- padding: 0.25rem 0.5rem;
45
- font-size: 0.6875rem;
46
- font-weight: 600;
47
- text-transform: uppercase;
48
- letter-spacing: 0.05em;
49
- color: rgb(255 255 255 / 0.4);
50
- }
51
- .option {
52
- display: flex;
53
- align-items: center;
54
- width: 100%;
55
- padding: 0.375rem 0.5rem;
56
- border: none;
57
- background: none;
58
- color: rgb(255 255 255 / 0.7);
59
- font-size: 0.8125rem;
60
- cursor: pointer;
61
- border-radius: 0.25rem;
62
- text-align: left;
63
- }
64
- .option:hover {
65
- background: rgb(255 255 255 / 0.1);
66
- color: white;
67
- }
68
- .option--active {
69
- color: hsl(var(--tn-blue, 217 89% 61%));
70
- }
71
- .dot {
72
- width: 6px;
73
- height: 6px;
74
- border-radius: 50%;
75
- background: hsl(var(--tn-blue, 217 89% 61%));
76
- margin-right: 0.5rem;
77
- }
78
- .dot--hidden {
79
- visibility: hidden;
80
- }
81
36
  `,
82
37
  ];
83
38
 
39
+ protected updated(): void {
40
+ if (!this.open) {
41
+ return;
42
+ }
43
+
44
+ if (Number.isFinite(this.playbackRate)) {
45
+ this._playbackRate = this.playbackRate as number;
46
+ return;
47
+ }
48
+
49
+ const video = this.pc?.s.videoElement;
50
+ if (video && Number.isFinite(video.playbackRate)) {
51
+ this._playbackRate = video.playbackRate;
52
+ }
53
+ }
54
+
55
+ private _close(): void {
56
+ this.dispatchEvent(new CustomEvent("fw-close", { bubbles: true, composed: true }));
57
+ }
58
+
59
+ private _handleModeChange(mode: "auto" | "low-latency" | "quality"): void {
60
+ this.pc.setDevModeOptions({ playbackMode: mode });
61
+ this.dispatchEvent(
62
+ new CustomEvent("fw-mode-change", {
63
+ detail: { mode },
64
+ bubbles: true,
65
+ composed: true,
66
+ })
67
+ );
68
+ this._close();
69
+ }
70
+
71
+ private _handleSpeedChange(rate: number): void {
72
+ this._playbackRate = rate;
73
+ this.pc.setPlaybackRate(rate);
74
+ this.dispatchEvent(
75
+ new CustomEvent("fw-speed-change", {
76
+ detail: { rate },
77
+ bubbles: true,
78
+ composed: true,
79
+ })
80
+ );
81
+ this._close();
82
+ }
83
+
84
+ private _handleQualityChange(id: string): void {
85
+ this.pc.selectQuality(id);
86
+ this.dispatchEvent(
87
+ new CustomEvent("fw-quality-change", {
88
+ detail: { quality: id },
89
+ bubbles: true,
90
+ composed: true,
91
+ })
92
+ );
93
+ this._close();
94
+ }
95
+
96
+ private _handleCaptionChange(id: string): void {
97
+ if (id === "none") {
98
+ this.pc.selectTextTrack(null);
99
+ } else {
100
+ this.pc.selectTextTrack(id);
101
+ }
102
+ this.dispatchEvent(
103
+ new CustomEvent("fw-caption-change", {
104
+ detail: { caption: id },
105
+ bubbles: true,
106
+ composed: true,
107
+ })
108
+ );
109
+ this._close();
110
+ }
111
+
112
+ private _deriveFallbackQualities(): Array<{
113
+ id: string;
114
+ label: string;
115
+ bitrate?: number;
116
+ width?: number;
117
+ height?: number;
118
+ isAuto?: boolean;
119
+ active?: boolean;
120
+ }> {
121
+ const tracks = (
122
+ this.pc?.s.streamState?.streamInfo as
123
+ | {
124
+ meta?: {
125
+ tracks?: Record<
126
+ string,
127
+ { type?: string; codec?: string; width?: number; height?: number; bps?: number }
128
+ >;
129
+ };
130
+ }
131
+ | undefined
132
+ )?.meta?.tracks;
133
+
134
+ if (!tracks) {
135
+ return [];
136
+ }
137
+
138
+ return Object.entries(tracks)
139
+ .filter(([, track]) => track?.type === "video")
140
+ .map(([id, track]) => ({
141
+ id,
142
+ label: track.height ? `${track.height}p` : (track.codec ?? id),
143
+ width: track.width,
144
+ height: track.height,
145
+ bitrate: track.bps,
146
+ }))
147
+ .sort((a, b) => (b.height ?? 0) - (a.height ?? 0));
148
+ }
149
+
84
150
  protected render() {
85
- if (!this.open) return nothing;
86
- const { qualities } = this.pc.s;
87
- const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2];
151
+ if (!this.open) {
152
+ return nothing;
153
+ }
154
+
155
+ const state = this.pc.s;
156
+ const controllerQualities = state.qualities ?? [];
157
+ const qualities =
158
+ controllerQualities.length > 0 ? controllerQualities : this._deriveFallbackQualities();
159
+ const textTracks = state.textTracks ?? [];
160
+ const activeQuality =
161
+ this.qualityValue ?? qualities.find((quality) => quality.active)?.id ?? "auto";
162
+ const activeCaption =
163
+ this.captionValue ?? textTracks.find((track) => track.active)?.id ?? "none";
164
+
165
+ const supportsPlaybackRate =
166
+ this.supportsPlaybackRate ?? coreSupportsPlaybackRate(state.videoElement);
88
167
 
89
168
  return html`
90
- <div class="menu fw-settings-menu">
169
+ <div class="fw-player-surface fw-settings-menu" role="menu" aria-label="Player settings">
170
+ ${this.isContentLive
171
+ ? html`
172
+ <div class="fw-settings-section">
173
+ <div class="fw-settings-label">Mode</div>
174
+ <div class="fw-settings-options">
175
+ ${(["auto", "low-latency", "quality"] as const).map(
176
+ (mode) => html`
177
+ <button
178
+ type="button"
179
+ class=${classMap({
180
+ "fw-settings-btn": true,
181
+ "fw-settings-btn--active": this.playbackMode === mode,
182
+ })}
183
+ @click=${() => this._handleModeChange(mode)}
184
+ >
185
+ ${mode === "low-latency" ? "Fast" : mode === "quality" ? "Stable" : "Auto"}
186
+ </button>
187
+ `
188
+ )}
189
+ </div>
190
+ </div>
191
+ `
192
+ : nothing}
193
+ ${supportsPlaybackRate
194
+ ? html`
195
+ <div class="fw-settings-section">
196
+ <div class="fw-settings-label">Speed</div>
197
+ <div class="fw-settings-options fw-settings-options--wrap">
198
+ ${SPEED_PRESETS.map(
199
+ (rate) => html`
200
+ <button
201
+ type="button"
202
+ class=${classMap({
203
+ "fw-settings-btn": true,
204
+ "fw-settings-btn--active": this._playbackRate === rate,
205
+ })}
206
+ @click=${() => this._handleSpeedChange(rate)}
207
+ >
208
+ ${rate}x
209
+ </button>
210
+ `
211
+ )}
212
+ </div>
213
+ </div>
214
+ `
215
+ : nothing}
91
216
  ${qualities.length > 0
92
217
  ? html`
93
- <div class="section">
94
- <div class="label">Quality</div>
95
- ${qualities.map(
96
- (q) => html`
97
- <button
98
- class=${classMap({ option: true, "option--active": !!q.active })}
99
- @click=${() => this.pc.selectQuality(q.id)}
100
- >
101
- <div class=${classMap({ dot: true, "dot--hidden": !q.active })}></div>
102
- ${q.label}
103
- </button>
104
- `
105
- )}
218
+ <div class="fw-settings-section">
219
+ <div class="fw-settings-label">Quality</div>
220
+ <div class="fw-settings-list">
221
+ <button
222
+ type="button"
223
+ class=${classMap({
224
+ "fw-settings-list-item": true,
225
+ "fw-settings-list-item--active": activeQuality === "auto",
226
+ })}
227
+ @click=${() => this._handleQualityChange("auto")}
228
+ >
229
+ Auto
230
+ </button>
231
+ ${qualities.map(
232
+ (quality) => html`
233
+ <button
234
+ type="button"
235
+ class=${classMap({
236
+ "fw-settings-list-item": true,
237
+ "fw-settings-list-item--active": activeQuality === quality.id,
238
+ })}
239
+ @click=${() => this._handleQualityChange(quality.id)}
240
+ >
241
+ ${quality.label}
242
+ </button>
243
+ `
244
+ )}
245
+ </div>
106
246
  </div>
107
247
  `
108
248
  : nothing}
109
- <div class="section">
110
- <div class="label">Speed</div>
111
- ${speeds.map(
112
- (s) => html`
113
- <button class="option" @click=${() => this.pc.getController()?.setPlaybackRate(s)}>
114
- ${s === 1 ? "Normal" : `${s}x`}
115
- </button>
249
+ ${textTracks.length > 0
250
+ ? html`
251
+ <div class="fw-settings-section">
252
+ <div class="fw-settings-label">Captions</div>
253
+ <div class="fw-settings-list">
254
+ <button
255
+ type="button"
256
+ class=${classMap({
257
+ "fw-settings-list-item": true,
258
+ "fw-settings-list-item--active": activeCaption === "none",
259
+ })}
260
+ @click=${() => this._handleCaptionChange("none")}
261
+ >
262
+ Off
263
+ </button>
264
+ ${textTracks.map(
265
+ (track) => html`
266
+ <button
267
+ type="button"
268
+ class=${classMap({
269
+ "fw-settings-list-item": true,
270
+ "fw-settings-list-item--active": activeCaption === track.id,
271
+ })}
272
+ @click=${() => this._handleCaptionChange(track.id)}
273
+ >
274
+ ${track.label || track.id}
275
+ </button>
276
+ `
277
+ )}
278
+ </div>
279
+ </div>
116
280
  `
117
- )}
118
- </div>
281
+ : nothing}
119
282
  </div>
120
283
  `;
121
284
  }
@@ -1,14 +1,18 @@
1
1
  /**
2
- * <fw-stats-panel> — Stats for nerds overlay.
3
- * Port of StatsPanel.tsx from player-react.
2
+ * <fw-stats-panel> — Stats for nerds overlay aligned with wrapper diagnostics.
4
3
  */
5
- import { LitElement, html, css, nothing } from "lit";
4
+ import { LitElement, html, css } from "lit";
6
5
  import { customElement, property } from "lit/decorators.js";
7
6
  import { sharedStyles } from "../styles/shared-styles.js";
8
7
  import { utilityStyles } from "../styles/utility-styles.js";
9
8
  import { closeIcon } from "../icons/index.js";
10
9
  import type { PlayerControllerHost } from "../controllers/player-controller-host.js";
11
10
 
11
+ interface StatRow {
12
+ label: string;
13
+ value: string;
14
+ }
15
+
12
16
  @customElement("fw-stats-panel")
13
17
  export class FwStatsPanel extends LitElement {
14
18
  @property({ attribute: false }) pc!: PlayerControllerHost;
@@ -22,126 +26,195 @@ export class FwStatsPanel extends LitElement {
22
26
  }
23
27
  .panel {
24
28
  position: absolute;
25
- top: 0.75rem;
26
- left: 0.75rem;
29
+ top: 0.5rem;
30
+ right: 0.5rem;
27
31
  z-index: 30;
28
- min-width: 240px;
29
- max-width: 320px;
32
+ width: 18rem;
30
33
  max-height: 80%;
31
- overflow: auto;
32
- border-radius: 0.5rem;
33
- border: 1px solid rgb(255 255 255 / 0.1);
34
- background: rgb(0 0 0 / 0.85);
35
- backdrop-filter: blur(8px);
36
- padding: 0.5rem 0.75rem;
37
- font-size: 0.6875rem;
38
- color: rgb(255 255 255 / 0.7);
34
+ overflow-y: auto;
35
+ background: hsl(var(--tn-bg-dark) / 0.9);
36
+ backdrop-filter: blur(4px);
37
+ border: 1px solid hsl(var(--tn-fg-gutter) / 0.3);
38
+ font-family: ui-monospace, monospace;
39
+ font-size: 0.75rem;
40
+ color: hsl(var(--tn-fg));
39
41
  }
40
42
  .header {
41
43
  display: flex;
42
44
  align-items: center;
43
45
  justify-content: space-between;
44
- margin-bottom: 0.5rem;
46
+ padding: 0.5rem;
47
+ border-bottom: 1px solid hsl(var(--tn-fg-gutter) / 0.3);
45
48
  }
46
49
  .title {
47
- font-size: 0.75rem;
50
+ font-size: 10px;
48
51
  font-weight: 600;
49
- color: white;
52
+ text-transform: uppercase;
53
+ letter-spacing: 0.05em;
54
+ color: hsl(var(--tn-fg-dark));
50
55
  }
51
56
  .close {
52
57
  display: flex;
53
- background: none;
58
+ align-items: center;
59
+ justify-content: center;
60
+ width: 1.5rem;
61
+ height: 1.5rem;
54
62
  border: none;
55
- color: rgb(255 255 255 / 0.5);
63
+ background: transparent;
64
+ color: hsl(var(--tn-fg-dark));
56
65
  cursor: pointer;
57
- padding: 0;
58
66
  }
59
67
  .close:hover {
60
- color: white;
68
+ color: hsl(var(--tn-fg));
69
+ }
70
+ .rows {
71
+ padding: 0.5rem;
61
72
  }
62
73
  .row {
63
74
  display: flex;
64
75
  justify-content: space-between;
76
+ gap: 0.5rem;
65
77
  padding: 0.125rem 0;
66
78
  }
67
79
  .label {
68
- color: rgb(255 255 255 / 0.5);
80
+ color: hsl(var(--tn-fg-dark));
69
81
  }
70
82
  .value {
71
- color: rgb(255 255 255 / 0.9);
72
- font-variant-numeric: tabular-nums;
73
- font-family: ui-monospace, monospace;
74
- }
75
- .sep {
76
- height: 1px;
77
- background: rgb(255 255 255 / 0.08);
78
- margin: 0.375rem 0;
83
+ color: hsl(var(--tn-fg));
84
+ text-align: right;
85
+ word-break: break-word;
79
86
  }
80
87
  `,
81
88
  ];
82
89
 
83
- private _resolution(): string | null {
84
- const video = this.pc.s.videoElement;
85
- if (!video || !video.videoWidth || !video.videoHeight) return null;
86
- return `${video.videoWidth}x${video.videoHeight}`;
90
+ private _deriveTracksFromMist(mistInfo: any) {
91
+ const mistTracks = mistInfo?.meta?.tracks;
92
+ if (!mistTracks) return undefined;
93
+ return Object.values(mistTracks as Record<string, any>).map((track: any) => ({
94
+ type: track.type,
95
+ codec: track.codec,
96
+ width: track.width,
97
+ height: track.height,
98
+ bitrate: typeof track.bps === "number" ? Math.round(track.bps) : undefined,
99
+ fps: typeof track.fpks === "number" ? track.fpks / 1000 : undefined,
100
+ channels: track.channels,
101
+ sampleRate: track.rate,
102
+ }));
87
103
  }
88
104
 
89
- private _stat(label: string, value: string | number | null | undefined) {
90
- if (value == null || value === "") return nothing;
91
- return html`<div class="row">
92
- <span class="label">${label}</span><span class="value">${value}</span>
93
- </div>`;
105
+ private _formatTracks(metadata: any, mistInfo: any): string {
106
+ const tracks = metadata?.tracks ?? this._deriveTracksFromMist(mistInfo);
107
+ if (!tracks?.length) return "";
108
+ return tracks
109
+ .map((track: any) => {
110
+ if (track.type === "video") {
111
+ const resolution = track.width && track.height ? `${track.width}x${track.height}` : "?";
112
+ const bitrate = track.bitrate ? `${Math.round(track.bitrate / 1000)}kbps` : "?";
113
+ return `${track.codec ?? "?"} ${resolution}@${bitrate}`;
114
+ }
115
+ const channels = track.channels ? `${track.channels}ch` : "?";
116
+ return `${track.codec ?? "?"} ${channels}`;
117
+ })
118
+ .join(", ");
94
119
  }
95
120
 
96
- protected render() {
121
+ private _collectStats(): StatRow[] {
97
122
  const s = this.pc.s;
98
- const q = s.playbackQuality;
99
- const meta = s.metadata;
100
- const ss = s.streamState;
123
+ const video = s.videoElement;
124
+ const quality = s.playbackQuality;
125
+ const metadata = s.metadata;
126
+ const streamState = s.streamState;
127
+ const primaryEndpoint = s.endpoints?.primary as
128
+ | { protocol?: string; nodeId?: string; geoDistance?: number }
129
+ | undefined;
130
+
131
+ const currentRes = video ? `${video.videoWidth}x${video.videoHeight}` : "—";
132
+ const buffered =
133
+ video && video.buffered.length > 0
134
+ ? (video.buffered.end(video.buffered.length - 1) - video.currentTime).toFixed(1)
135
+ : "—";
136
+ const playbackRate = video?.playbackRate?.toFixed(2) ?? "1.00";
137
+ const qualityScore = quality?.score?.toFixed(0) ?? "—";
138
+ const bitrateKbps = quality?.bitrate ? `${(quality.bitrate / 1000).toFixed(0)} kbps` : "—";
139
+ const frameDropRate = quality?.frameDropRate?.toFixed(1) ?? "—";
140
+ const stallCount = quality?.stallCount ?? 0;
141
+ const latency = quality?.latency ? `${Math.round(quality.latency)} ms` : "—";
142
+ const viewers = metadata?.viewers ?? "—";
143
+ const streamStatus = streamState?.status ?? metadata?.status ?? "—";
144
+ const mistInfo = metadata?.mist ?? streamState?.streamInfo;
145
+ const mistType = mistInfo?.type ?? "—";
146
+ const mistBufferWindow = mistInfo?.meta?.buffer_window;
147
+ const mistLastMs = mistInfo?.lastms;
148
+ const mistUnixOffset = mistInfo?.unixoffset;
149
+
150
+ const stats: StatRow[] = [
151
+ { label: "Resolution", value: currentRes },
152
+ { label: "Buffer", value: `${buffered}s` },
153
+ { label: "Latency", value: latency },
154
+ { label: "Bitrate", value: bitrateKbps },
155
+ { label: "Quality Score", value: `${qualityScore}/100` },
156
+ { label: "Frame Drop Rate", value: `${frameDropRate}%` },
157
+ { label: "Stalls", value: String(stallCount) },
158
+ { label: "Playback Rate", value: `${playbackRate}x` },
159
+ { label: "Protocol", value: primaryEndpoint?.protocol ?? "—" },
160
+ { label: "Node", value: primaryEndpoint?.nodeId ?? "—" },
161
+ {
162
+ label: "Geo Distance",
163
+ value: primaryEndpoint?.geoDistance ? `${primaryEndpoint.geoDistance.toFixed(0)} km` : "—",
164
+ },
165
+ { label: "Viewers", value: String(viewers) },
166
+ { label: "Status", value: streamStatus },
167
+ { label: "Tracks", value: this._formatTracks(metadata, mistInfo) },
168
+ { label: "Mist Type", value: mistType },
169
+ {
170
+ label: "Mist Buffer Window",
171
+ value: mistBufferWindow != null ? String(mistBufferWindow) : "—",
172
+ },
173
+ { label: "Mist Lastms", value: mistLastMs != null ? String(mistLastMs) : "—" },
174
+ { label: "Mist Unixoffset", value: mistUnixOffset != null ? String(mistUnixOffset) : "—" },
175
+ ];
176
+
177
+ if (metadata?.title) {
178
+ stats.unshift({ label: "Title", value: metadata.title });
179
+ }
180
+ if (metadata?.durationSeconds) {
181
+ const mins = Math.floor(metadata.durationSeconds / 60);
182
+ const secs = metadata.durationSeconds % 60;
183
+ stats.push({ label: "Duration", value: `${mins}:${String(secs).padStart(2, "0")}` });
184
+ }
185
+ if (metadata?.recordingSizeBytes) {
186
+ const mb = (metadata.recordingSizeBytes / (1024 * 1024)).toFixed(1);
187
+ stats.push({ label: "Size", value: `${mb} MB` });
188
+ }
189
+
190
+ return stats;
191
+ }
192
+
193
+ protected render() {
194
+ const stats = this._collectStats();
101
195
 
102
196
  return html`
103
197
  <div class="panel fw-stats-panel">
104
- <div class="header">
105
- <span class="title">Stats</span>
198
+ <div class="header fw-stats-header">
199
+ <span class="title">Stats Overlay</span>
106
200
  <button
107
201
  class="close"
108
202
  @click=${() =>
109
203
  this.dispatchEvent(new CustomEvent("fw-close", { bubbles: true, composed: true }))}
110
- aria-label="Close stats"
204
+ aria-label="Close stats panel"
111
205
  >
112
206
  ${closeIcon()}
113
207
  </button>
114
208
  </div>
115
-
116
- ${this._stat("State", s.state)} ${this._stat("Player", s.currentPlayerInfo?.name)}
117
- ${this._stat("Source", s.currentSourceInfo?.type)}
118
-
119
- <div class="sep"></div>
120
-
121
- ${q
122
- ? html`
123
- ${this._stat("Resolution", this._resolution())}
124
- ${this._stat("Bitrate", q.bitrate ? `${Math.round(q.bitrate / 1000)} kbps` : null)}
125
- ${this._stat("Latency", q.latency != null ? `${q.latency.toFixed(1)}s` : null)}
126
- ${this._stat(
127
- "Buffer",
128
- q.bufferedAhead != null ? `${q.bufferedAhead.toFixed(1)}s` : null
129
- )}
130
- ${this._stat("Quality", q.score != null ? `${q.score.toFixed(0)}` : null)}
131
- ${this._stat(
132
- "Frame drops",
133
- q.frameDropRate != null ? `${q.frameDropRate.toFixed(1)}%` : null
134
- )}
135
- ${this._stat("Stalls", q.stallCount ?? null)}
136
- `
137
- : nothing}
138
- ${meta || ss
139
- ? html`
140
- <div class="sep"></div>
141
- ${this._stat("Viewers", meta?.viewers ?? null)}
142
- ${this._stat("Stream status", ss?.status ?? null)}
143
- `
144
- : nothing}
209
+ <div class="rows">
210
+ ${stats.map(
211
+ (stat) =>
212
+ html`<div class="row fw-stats-row">
213
+ <span class="label">${stat.label}</span>
214
+ <span class="value fw-stats-value">${stat.value}</span>
215
+ </div>`
216
+ )}
217
+ </div>
145
218
  </div>
146
219
  `;
147
220
  }