@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.
- package/dist/cjs/components/fw-dev-mode-panel.js +845 -212
- package/dist/cjs/components/fw-dev-mode-panel.js.map +1 -1
- package/dist/cjs/components/fw-dvd-logo.js +211 -0
- package/dist/cjs/components/fw-dvd-logo.js.map +1 -0
- package/dist/cjs/components/fw-idle-screen.js +641 -97
- package/dist/cjs/components/fw-idle-screen.js.map +1 -1
- package/dist/cjs/components/fw-loading-screen.js +513 -0
- package/dist/cjs/components/fw-loading-screen.js.map +1 -0
- package/dist/cjs/components/fw-player-controls.js +390 -173
- package/dist/cjs/components/fw-player-controls.js.map +1 -1
- package/dist/cjs/components/fw-player.js +506 -63
- package/dist/cjs/components/fw-player.js.map +1 -1
- package/dist/cjs/components/fw-seek-bar.js +292 -142
- package/dist/cjs/components/fw-seek-bar.js.map +1 -1
- package/dist/cjs/components/fw-settings-menu.js +208 -81
- package/dist/cjs/components/fw-settings-menu.js.map +1 -1
- package/dist/cjs/components/fw-stats-panel.js +134 -70
- package/dist/cjs/components/fw-stats-panel.js.map +1 -1
- package/dist/cjs/components/fw-stream-state-overlay.js +338 -0
- package/dist/cjs/components/fw-stream-state-overlay.js.map +1 -0
- package/dist/cjs/components/fw-subtitle-renderer.js +174 -27
- package/dist/cjs/components/fw-subtitle-renderer.js.map +1 -1
- package/dist/cjs/components/fw-thumbnail-overlay.js +161 -0
- package/dist/cjs/components/fw-thumbnail-overlay.js.map +1 -0
- package/dist/cjs/components/fw-volume-control.js +150 -69
- package/dist/cjs/components/fw-volume-control.js.map +1 -1
- package/dist/cjs/components/shared/hitmarker-audio.js +76 -0
- package/dist/cjs/components/shared/hitmarker-audio.js.map +1 -0
- package/dist/cjs/constants/media-assets.js +11 -0
- package/dist/cjs/constants/media-assets.js.map +1 -0
- package/dist/cjs/controllers/player-controller-host.js +51 -2
- package/dist/cjs/controllers/player-controller-host.js.map +1 -1
- package/dist/cjs/define.js +8 -0
- package/dist/cjs/define.js.map +1 -1
- package/dist/cjs/icons/index.js +27 -0
- package/dist/cjs/icons/index.js.map +1 -1
- package/dist/cjs/index.js +20 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/components/fw-dev-mode-panel.js +846 -213
- package/dist/esm/components/fw-dev-mode-panel.js.map +1 -1
- package/dist/esm/components/fw-dvd-logo.js +211 -0
- package/dist/esm/components/fw-dvd-logo.js.map +1 -0
- package/dist/esm/components/fw-idle-screen.js +643 -99
- package/dist/esm/components/fw-idle-screen.js.map +1 -1
- package/dist/esm/components/fw-loading-screen.js +513 -0
- package/dist/esm/components/fw-loading-screen.js.map +1 -0
- package/dist/esm/components/fw-player-controls.js +391 -174
- package/dist/esm/components/fw-player-controls.js.map +1 -1
- package/dist/esm/components/fw-player.js +506 -63
- package/dist/esm/components/fw-player.js.map +1 -1
- package/dist/esm/components/fw-seek-bar.js +293 -143
- package/dist/esm/components/fw-seek-bar.js.map +1 -1
- package/dist/esm/components/fw-settings-menu.js +209 -82
- package/dist/esm/components/fw-settings-menu.js.map +1 -1
- package/dist/esm/components/fw-stats-panel.js +135 -71
- package/dist/esm/components/fw-stats-panel.js.map +1 -1
- package/dist/esm/components/fw-stream-state-overlay.js +338 -0
- package/dist/esm/components/fw-stream-state-overlay.js.map +1 -0
- package/dist/esm/components/fw-subtitle-renderer.js +175 -28
- package/dist/esm/components/fw-subtitle-renderer.js.map +1 -1
- package/dist/esm/components/fw-thumbnail-overlay.js +161 -0
- package/dist/esm/components/fw-thumbnail-overlay.js.map +1 -0
- package/dist/esm/components/fw-volume-control.js +150 -69
- package/dist/esm/components/fw-volume-control.js.map +1 -1
- package/dist/esm/components/shared/hitmarker-audio.js +74 -0
- package/dist/esm/components/shared/hitmarker-audio.js.map +1 -0
- package/dist/esm/constants/media-assets.js +8 -0
- package/dist/esm/constants/media-assets.js.map +1 -0
- package/dist/esm/controllers/player-controller-host.js +51 -2
- package/dist/esm/controllers/player-controller-host.js.map +1 -1
- package/dist/esm/define.js +8 -0
- package/dist/esm/define.js.map +1 -1
- package/dist/esm/icons/index.js +26 -2
- package/dist/esm/icons/index.js.map +1 -1
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/fw-player.iife.js +2097 -883
- package/dist/types/components/fw-dev-mode-panel.d.ts +36 -9
- package/dist/types/components/fw-dvd-logo.d.ts +29 -0
- package/dist/types/components/fw-idle-screen.d.ts +36 -0
- package/dist/types/components/fw-loading-screen.d.ts +36 -0
- package/dist/types/components/fw-player-controls.d.ts +23 -6
- package/dist/types/components/fw-player.d.ts +32 -1
- package/dist/types/components/fw-seek-bar.d.ts +31 -14
- package/dist/types/components/fw-settings-menu.d.ts +16 -1
- package/dist/types/components/fw-stats-panel.d.ts +4 -4
- package/dist/types/components/fw-stream-state-overlay.d.ts +20 -0
- package/dist/types/components/fw-subtitle-renderer.d.ts +33 -2
- package/dist/types/components/fw-thumbnail-overlay.d.ts +17 -0
- package/dist/types/components/fw-volume-control.d.ts +11 -4
- package/dist/types/components/shared/hitmarker-audio.d.ts +1 -0
- package/dist/types/constants/media-assets.d.ts +5 -0
- package/dist/types/controllers/player-controller-host.d.ts +14 -1
- package/dist/types/iife-entry.d.ts +4 -0
- package/dist/types/index.d.ts +4 -0
- package/package.json +2 -2
- package/src/components/fw-dev-mode-panel.ts +929 -228
- package/src/components/fw-dvd-logo.ts +233 -0
- package/src/components/fw-idle-screen.ts +680 -100
- package/src/components/fw-loading-screen.ts +540 -0
- package/src/components/fw-player-controls.ts +475 -175
- package/src/components/fw-player.ts +551 -60
- package/src/components/fw-seek-bar.ts +336 -143
- package/src/components/fw-settings-menu.ts +248 -85
- package/src/components/fw-stats-panel.ts +150 -77
- package/src/components/fw-stream-state-overlay.ts +331 -0
- package/src/components/fw-subtitle-renderer.ts +216 -28
- package/src/components/fw-thumbnail-overlay.ts +148 -0
- package/src/components/fw-volume-control.ts +166 -66
- package/src/components/shared/hitmarker-audio.ts +92 -0
- package/src/constants/media-assets.ts +7 -0
- package/src/controllers/player-controller-host.ts +52 -3
- package/src/define.ts +8 -0
- package/src/iife-entry.ts +4 -0
- package/src/index.ts +4 -0
- package/dist/fw-player.iife.js.map +0 -1
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* <fw-settings-menu> —
|
|
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)
|
|
86
|
-
|
|
87
|
-
|
|
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="
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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.
|
|
26
|
-
|
|
29
|
+
top: 0.5rem;
|
|
30
|
+
right: 0.5rem;
|
|
27
31
|
z-index: 30;
|
|
28
|
-
|
|
29
|
-
max-width: 320px;
|
|
32
|
+
width: 18rem;
|
|
30
33
|
max-height: 80%;
|
|
31
|
-
overflow: auto;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
46
|
+
padding: 0.5rem;
|
|
47
|
+
border-bottom: 1px solid hsl(var(--tn-fg-gutter) / 0.3);
|
|
45
48
|
}
|
|
46
49
|
.title {
|
|
47
|
-
font-size:
|
|
50
|
+
font-size: 10px;
|
|
48
51
|
font-weight: 600;
|
|
49
|
-
|
|
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
|
-
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
width: 1.5rem;
|
|
61
|
+
height: 1.5rem;
|
|
54
62
|
border: none;
|
|
55
|
-
|
|
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:
|
|
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:
|
|
80
|
+
color: hsl(var(--tn-fg-dark));
|
|
69
81
|
}
|
|
70
82
|
.value {
|
|
71
|
-
color:
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
84
|
-
const
|
|
85
|
-
if (!
|
|
86
|
-
return
|
|
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
|
|
90
|
-
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
121
|
+
private _collectStats(): StatRow[] {
|
|
97
122
|
const s = this.pc.s;
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
}
|