@livepeer-frameworks/player-wc 0.1.1 → 0.1.3

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 +347 -173
  10. package/dist/cjs/components/fw-player-controls.js.map +1 -1
  11. package/dist/cjs/components/fw-player.js +460 -60
  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 +191 -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 +28 -1
  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 +348 -174
  48. package/dist/esm/components/fw-player-controls.js.map +1 -1
  49. package/dist/esm/components/fw-player.js +460 -60
  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 +192 -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 +28 -1
  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 +2072 -880
  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 +21 -6
  83. package/dist/types/components/fw-player.d.ts +28 -1
  84. package/dist/types/components/fw-seek-bar.d.ts +31 -14
  85. package/dist/types/components/fw-settings-menu.d.ts +15 -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 +3 -3
  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 +435 -176
  102. package/src/components/fw-player.ts +505 -57
  103. package/src/components/fw-seek-bar.ts +336 -143
  104. package/src/components/fw-settings-menu.ts +208 -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 +29 -2
  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,212 @@ 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
+
84
112
  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];
113
+ if (!this.open) {
114
+ return nothing;
115
+ }
116
+
117
+ const state = this.pc.s;
118
+ const qualities = state.qualities ?? [];
119
+ const textTracks = state.textTracks ?? [];
120
+ const activeQuality =
121
+ this.qualityValue ?? qualities.find((quality) => quality.active)?.id ?? "auto";
122
+ const activeCaption =
123
+ this.captionValue ?? textTracks.find((track) => track.active)?.id ?? "none";
124
+
125
+ const supportsPlaybackRate =
126
+ this.supportsPlaybackRate ?? coreSupportsPlaybackRate(state.videoElement);
88
127
 
89
128
  return html`
90
- <div class="menu fw-settings-menu">
129
+ <div class="fw-player-surface fw-settings-menu" role="menu" aria-label="Player settings">
130
+ ${this.isContentLive
131
+ ? html`
132
+ <div class="fw-settings-section">
133
+ <div class="fw-settings-label">Mode</div>
134
+ <div class="fw-settings-options">
135
+ ${(["auto", "low-latency", "quality"] as const).map(
136
+ (mode) => html`
137
+ <button
138
+ type="button"
139
+ class=${classMap({
140
+ "fw-settings-btn": true,
141
+ "fw-settings-btn--active": this.playbackMode === mode,
142
+ })}
143
+ @click=${() => this._handleModeChange(mode)}
144
+ >
145
+ ${mode === "low-latency" ? "Fast" : mode === "quality" ? "Stable" : "Auto"}
146
+ </button>
147
+ `
148
+ )}
149
+ </div>
150
+ </div>
151
+ `
152
+ : nothing}
153
+ ${supportsPlaybackRate
154
+ ? html`
155
+ <div class="fw-settings-section">
156
+ <div class="fw-settings-label">Speed</div>
157
+ <div class="fw-settings-options fw-settings-options--wrap">
158
+ ${SPEED_PRESETS.map(
159
+ (rate) => html`
160
+ <button
161
+ type="button"
162
+ class=${classMap({
163
+ "fw-settings-btn": true,
164
+ "fw-settings-btn--active": this._playbackRate === rate,
165
+ })}
166
+ @click=${() => this._handleSpeedChange(rate)}
167
+ >
168
+ ${rate}x
169
+ </button>
170
+ `
171
+ )}
172
+ </div>
173
+ </div>
174
+ `
175
+ : nothing}
91
176
  ${qualities.length > 0
92
177
  ? 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
- )}
178
+ <div class="fw-settings-section">
179
+ <div class="fw-settings-label">Quality</div>
180
+ <div class="fw-settings-list">
181
+ <button
182
+ type="button"
183
+ class=${classMap({
184
+ "fw-settings-list-item": true,
185
+ "fw-settings-list-item--active": activeQuality === "auto",
186
+ })}
187
+ @click=${() => this._handleQualityChange("auto")}
188
+ >
189
+ Auto
190
+ </button>
191
+ ${qualities.map(
192
+ (quality) => html`
193
+ <button
194
+ type="button"
195
+ class=${classMap({
196
+ "fw-settings-list-item": true,
197
+ "fw-settings-list-item--active": activeQuality === quality.id,
198
+ })}
199
+ @click=${() => this._handleQualityChange(quality.id)}
200
+ >
201
+ ${quality.label}
202
+ </button>
203
+ `
204
+ )}
205
+ </div>
106
206
  </div>
107
207
  `
108
208
  : 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>
209
+ ${textTracks.length > 0
210
+ ? html`
211
+ <div class="fw-settings-section">
212
+ <div class="fw-settings-label">Captions</div>
213
+ <div class="fw-settings-list">
214
+ <button
215
+ type="button"
216
+ class=${classMap({
217
+ "fw-settings-list-item": true,
218
+ "fw-settings-list-item--active": activeCaption === "none",
219
+ })}
220
+ @click=${() => this._handleCaptionChange("none")}
221
+ >
222
+ Off
223
+ </button>
224
+ ${textTracks.map(
225
+ (track) => html`
226
+ <button
227
+ type="button"
228
+ class=${classMap({
229
+ "fw-settings-list-item": true,
230
+ "fw-settings-list-item--active": activeCaption === track.id,
231
+ })}
232
+ @click=${() => this._handleCaptionChange(track.id)}
233
+ >
234
+ ${track.label || track.id}
235
+ </button>
236
+ `
237
+ )}
238
+ </div>
239
+ </div>
116
240
  `
117
- )}
118
- </div>
241
+ : nothing}
119
242
  </div>
120
243
  `;
121
244
  }
@@ -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
  }