@livepeer-frameworks/player-wc 0.1.0

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 (141) hide show
  1. package/dist/cjs/components/fw-context-menu.js +17 -0
  2. package/dist/cjs/components/fw-context-menu.js.map +1 -0
  3. package/dist/cjs/components/fw-dev-mode-panel.js +273 -0
  4. package/dist/cjs/components/fw-dev-mode-panel.js.map +1 -0
  5. package/dist/cjs/components/fw-error-overlay.js +101 -0
  6. package/dist/cjs/components/fw-error-overlay.js.map +1 -0
  7. package/dist/cjs/components/fw-idle-screen.js +182 -0
  8. package/dist/cjs/components/fw-idle-screen.js.map +1 -0
  9. package/dist/cjs/components/fw-loading-spinner.js +62 -0
  10. package/dist/cjs/components/fw-loading-spinner.js.map +1 -0
  11. package/dist/cjs/components/fw-player-controls.js +258 -0
  12. package/dist/cjs/components/fw-player-controls.js.map +1 -0
  13. package/dist/cjs/components/fw-player.js +570 -0
  14. package/dist/cjs/components/fw-player.js.map +1 -0
  15. package/dist/cjs/components/fw-seek-bar.js +233 -0
  16. package/dist/cjs/components/fw-seek-bar.js.map +1 -0
  17. package/dist/cjs/components/fw-settings-menu.js +126 -0
  18. package/dist/cjs/components/fw-settings-menu.js.map +1 -0
  19. package/dist/cjs/components/fw-skip-indicator.js +143 -0
  20. package/dist/cjs/components/fw-skip-indicator.js.map +1 -0
  21. package/dist/cjs/components/fw-speed-indicator.js +61 -0
  22. package/dist/cjs/components/fw-speed-indicator.js.map +1 -0
  23. package/dist/cjs/components/fw-stats-panel.js +141 -0
  24. package/dist/cjs/components/fw-stats-panel.js.map +1 -0
  25. package/dist/cjs/components/fw-subtitle-renderer.js +70 -0
  26. package/dist/cjs/components/fw-subtitle-renderer.js.map +1 -0
  27. package/dist/cjs/components/fw-title-overlay.js +72 -0
  28. package/dist/cjs/components/fw-title-overlay.js.map +1 -0
  29. package/dist/cjs/components/fw-toast.js +74 -0
  30. package/dist/cjs/components/fw-toast.js.map +1 -0
  31. package/dist/cjs/components/fw-volume-control.js +140 -0
  32. package/dist/cjs/components/fw-volume-control.js.map +1 -0
  33. package/dist/cjs/controllers/player-controller-host.js +315 -0
  34. package/dist/cjs/controllers/player-controller-host.js.map +1 -0
  35. package/dist/cjs/define.js +45 -0
  36. package/dist/cjs/define.js.map +1 -0
  37. package/dist/cjs/icons/index.js +153 -0
  38. package/dist/cjs/icons/index.js.map +1 -0
  39. package/dist/cjs/index.js +88 -0
  40. package/dist/cjs/index.js.map +1 -0
  41. package/dist/cjs/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 +33 -0
  42. package/dist/cjs/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.map +1 -0
  43. package/dist/cjs/styles/shared-styles.js +1967 -0
  44. package/dist/cjs/styles/shared-styles.js.map +1 -0
  45. package/dist/cjs/styles/utility-styles.js +725 -0
  46. package/dist/cjs/styles/utility-styles.js.map +1 -0
  47. package/dist/esm/components/fw-context-menu.js +17 -0
  48. package/dist/esm/components/fw-context-menu.js.map +1 -0
  49. package/dist/esm/components/fw-dev-mode-panel.js +273 -0
  50. package/dist/esm/components/fw-dev-mode-panel.js.map +1 -0
  51. package/dist/esm/components/fw-error-overlay.js +101 -0
  52. package/dist/esm/components/fw-error-overlay.js.map +1 -0
  53. package/dist/esm/components/fw-idle-screen.js +182 -0
  54. package/dist/esm/components/fw-idle-screen.js.map +1 -0
  55. package/dist/esm/components/fw-loading-spinner.js +62 -0
  56. package/dist/esm/components/fw-loading-spinner.js.map +1 -0
  57. package/dist/esm/components/fw-player-controls.js +258 -0
  58. package/dist/esm/components/fw-player-controls.js.map +1 -0
  59. package/dist/esm/components/fw-player.js +570 -0
  60. package/dist/esm/components/fw-player.js.map +1 -0
  61. package/dist/esm/components/fw-seek-bar.js +233 -0
  62. package/dist/esm/components/fw-seek-bar.js.map +1 -0
  63. package/dist/esm/components/fw-settings-menu.js +126 -0
  64. package/dist/esm/components/fw-settings-menu.js.map +1 -0
  65. package/dist/esm/components/fw-skip-indicator.js +143 -0
  66. package/dist/esm/components/fw-skip-indicator.js.map +1 -0
  67. package/dist/esm/components/fw-speed-indicator.js +61 -0
  68. package/dist/esm/components/fw-speed-indicator.js.map +1 -0
  69. package/dist/esm/components/fw-stats-panel.js +141 -0
  70. package/dist/esm/components/fw-stats-panel.js.map +1 -0
  71. package/dist/esm/components/fw-subtitle-renderer.js +70 -0
  72. package/dist/esm/components/fw-subtitle-renderer.js.map +1 -0
  73. package/dist/esm/components/fw-title-overlay.js +72 -0
  74. package/dist/esm/components/fw-title-overlay.js.map +1 -0
  75. package/dist/esm/components/fw-toast.js +74 -0
  76. package/dist/esm/components/fw-toast.js.map +1 -0
  77. package/dist/esm/components/fw-volume-control.js +140 -0
  78. package/dist/esm/components/fw-volume-control.js.map +1 -0
  79. package/dist/esm/controllers/player-controller-host.js +313 -0
  80. package/dist/esm/controllers/player-controller-host.js.map +1 -0
  81. package/dist/esm/define.js +43 -0
  82. package/dist/esm/define.js.map +1 -0
  83. package/dist/esm/icons/index.js +141 -0
  84. package/dist/esm/icons/index.js.map +1 -0
  85. package/dist/esm/index.js +18 -0
  86. package/dist/esm/index.js.map +1 -0
  87. package/dist/esm/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 +31 -0
  88. package/dist/esm/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.map +1 -0
  89. package/dist/esm/styles/shared-styles.js +1965 -0
  90. package/dist/esm/styles/shared-styles.js.map +1 -0
  91. package/dist/esm/styles/utility-styles.js +723 -0
  92. package/dist/esm/styles/utility-styles.js.map +1 -0
  93. package/dist/fw-player.iife.js +4362 -0
  94. package/dist/fw-player.iife.js.map +1 -0
  95. package/dist/types/components/fw-context-menu.d.ts +15 -0
  96. package/dist/types/components/fw-dev-mode-panel.d.ts +24 -0
  97. package/dist/types/components/fw-error-overlay.d.ts +14 -0
  98. package/dist/types/components/fw-idle-screen.d.ts +13 -0
  99. package/dist/types/components/fw-loading-spinner.d.ts +10 -0
  100. package/dist/types/components/fw-player-controls.d.ts +23 -0
  101. package/dist/types/components/fw-player.d.ts +74 -0
  102. package/dist/types/components/fw-seek-bar.d.ts +33 -0
  103. package/dist/types/components/fw-settings-menu.d.ts +16 -0
  104. package/dist/types/components/fw-skip-indicator.d.ts +18 -0
  105. package/dist/types/components/fw-speed-indicator.d.ts +11 -0
  106. package/dist/types/components/fw-stats-panel.d.ts +18 -0
  107. package/dist/types/components/fw-subtitle-renderer.d.ts +21 -0
  108. package/dist/types/components/fw-title-overlay.d.ts +12 -0
  109. package/dist/types/components/fw-toast.d.ts +12 -0
  110. package/dist/types/components/fw-volume-control.d.ts +18 -0
  111. package/dist/types/controllers/player-controller-host.d.ts +119 -0
  112. package/dist/types/define.d.ts +1 -0
  113. package/dist/types/icons/index.d.ts +23 -0
  114. package/dist/types/iife-entry.d.ts +11 -0
  115. package/dist/types/index.d.ts +25 -0
  116. package/dist/types/styles/shared-styles.d.ts +1 -0
  117. package/dist/types/styles/utility-styles.d.ts +1 -0
  118. package/package.json +65 -0
  119. package/src/components/fw-context-menu.ts +23 -0
  120. package/src/components/fw-dev-mode-panel.ts +285 -0
  121. package/src/components/fw-error-overlay.ts +96 -0
  122. package/src/components/fw-idle-screen.ts +182 -0
  123. package/src/components/fw-loading-spinner.ts +63 -0
  124. package/src/components/fw-player-controls.ts +256 -0
  125. package/src/components/fw-player.ts +557 -0
  126. package/src/components/fw-seek-bar.ts +219 -0
  127. package/src/components/fw-settings-menu.ts +128 -0
  128. package/src/components/fw-skip-indicator.ts +139 -0
  129. package/src/components/fw-speed-indicator.ts +57 -0
  130. package/src/components/fw-stats-panel.ts +154 -0
  131. package/src/components/fw-subtitle-renderer.ts +65 -0
  132. package/src/components/fw-title-overlay.ts +64 -0
  133. package/src/components/fw-toast.ts +70 -0
  134. package/src/components/fw-volume-control.ts +140 -0
  135. package/src/controllers/player-controller-host.ts +457 -0
  136. package/src/define.ts +43 -0
  137. package/src/icons/index.ts +209 -0
  138. package/src/iife-entry.ts +11 -0
  139. package/src/index.ts +31 -0
  140. package/src/styles/shared-styles.ts +1962 -0
  141. package/src/styles/utility-styles.ts +720 -0
@@ -0,0 +1,219 @@
1
+ /**
2
+ * <fw-seek-bar> — Video seek bar with buffer visualization, drag, tooltips.
3
+ * Port of SeekBar.tsx from player-react.
4
+ */
5
+ import { LitElement, html, css, nothing } from "lit";
6
+ import { customElement, property, state, query } from "lit/decorators.js";
7
+ import { classMap } from "lit/directives/class-map.js";
8
+ import { styleMap } from "lit/directives/style-map.js";
9
+ import { sharedStyles } from "../styles/shared-styles.js";
10
+
11
+ @customElement("fw-seek-bar")
12
+ export class FwSeekBar extends LitElement {
13
+ @property({ type: Number }) currentTime = 0;
14
+ @property({ type: Number }) duration = 0;
15
+ @property({ type: Boolean }) disabled = false;
16
+ @property({ type: Boolean, attribute: "is-live" }) isLive = false;
17
+
18
+ @state() private _hovering = false;
19
+ @state() private _dragging = false;
20
+ @state() private _hoverPct = 0;
21
+ @state() private _dragPct: number | null = null;
22
+
23
+ @query(".track") private _trackEl!: HTMLDivElement;
24
+
25
+ static styles = [
26
+ sharedStyles,
27
+ css`
28
+ :host {
29
+ display: block;
30
+ width: 100%;
31
+ }
32
+ .wrap {
33
+ position: relative;
34
+ padding: 0.25rem 0;
35
+ cursor: pointer;
36
+ }
37
+ .wrap--disabled {
38
+ opacity: 0.4;
39
+ pointer-events: none;
40
+ }
41
+ .track {
42
+ position: relative;
43
+ height: 4px;
44
+ background: rgb(255 255 255 / 0.15);
45
+ border-radius: 2px;
46
+ overflow: hidden;
47
+ transition: height 150ms ease;
48
+ }
49
+ .wrap:hover .track,
50
+ .track--active {
51
+ height: 6px;
52
+ }
53
+ .progress {
54
+ position: absolute;
55
+ top: 0;
56
+ left: 0;
57
+ height: 100%;
58
+ background: hsl(var(--tn-blue, 217 89% 61%));
59
+ border-radius: 2px;
60
+ pointer-events: none;
61
+ }
62
+ .thumb {
63
+ position: absolute;
64
+ top: 50%;
65
+ width: 12px;
66
+ height: 12px;
67
+ border-radius: 50%;
68
+ background: white;
69
+ transform: translate(-50%, -50%) scale(0);
70
+ transition: transform 150ms ease;
71
+ z-index: 5;
72
+ pointer-events: none;
73
+ }
74
+ .wrap:hover .thumb,
75
+ .thumb--active {
76
+ transform: translate(-50%, -50%) scale(1);
77
+ }
78
+ .tooltip {
79
+ position: absolute;
80
+ bottom: 100%;
81
+ margin-bottom: 8px;
82
+ transform: translateX(-50%);
83
+ padding: 0.25rem 0.5rem;
84
+ border-radius: 4px;
85
+ background: rgb(0 0 0 / 0.85);
86
+ color: white;
87
+ font-size: 0.6875rem;
88
+ font-variant-numeric: tabular-nums;
89
+ white-space: nowrap;
90
+ pointer-events: none;
91
+ z-index: 10;
92
+ }
93
+ `,
94
+ ];
95
+
96
+ private _pctFromEvent(e: MouseEvent | PointerEvent): number {
97
+ if (!this._trackEl) return 0;
98
+ const rect = this._trackEl.getBoundingClientRect();
99
+ return Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
100
+ }
101
+
102
+ private _timeFromPct(pct: number): number {
103
+ const d = this.isLive ? this.duration : this.duration;
104
+ return isFinite(d) && d > 0 ? pct * d : 0;
105
+ }
106
+
107
+ private _formatTime(seconds: number): string {
108
+ if (!isFinite(seconds)) return "0:00";
109
+ const abs = Math.abs(Math.floor(seconds));
110
+ const h = Math.floor(abs / 3600);
111
+ const m = Math.floor((abs % 3600) / 60);
112
+ const s = abs % 60;
113
+ const sign = seconds < 0 ? "-" : "";
114
+ if (h > 0) return `${sign}${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
115
+ return `${sign}${m}:${String(s).padStart(2, "0")}`;
116
+ }
117
+
118
+ private _handlePointerDown = (e: PointerEvent) => {
119
+ if (this.disabled) return;
120
+ e.preventDefault();
121
+ this._dragging = true;
122
+ this._dragPct = this._pctFromEvent(e);
123
+ (e.target as HTMLElement).setPointerCapture?.(e.pointerId);
124
+ };
125
+
126
+ private _handlePointerMove = (e: PointerEvent) => {
127
+ const pct = this._pctFromEvent(e);
128
+ if (this._dragging) {
129
+ this._dragPct = pct;
130
+ }
131
+ this._hoverPct = pct;
132
+ };
133
+
134
+ private _handlePointerUp = (e: PointerEvent) => {
135
+ if (this._dragging && this._dragPct != null) {
136
+ const time = this._timeFromPct(this._dragPct);
137
+ this.dispatchEvent(
138
+ new CustomEvent("fw-seek", { detail: { time }, bubbles: true, composed: true })
139
+ );
140
+ }
141
+ this._dragging = false;
142
+ this._dragPct = null;
143
+ };
144
+
145
+ private _handleClick = (e: MouseEvent) => {
146
+ if (this.disabled || this._dragging) return;
147
+ const pct = this._pctFromEvent(e);
148
+ const time = this._timeFromPct(pct);
149
+ this.dispatchEvent(
150
+ new CustomEvent("fw-seek", { detail: { time }, bubbles: true, composed: true })
151
+ );
152
+ };
153
+
154
+ private _handleMouseEnter = () => {
155
+ this._hovering = true;
156
+ };
157
+ private _handleMouseLeave = () => {
158
+ this._hovering = false;
159
+ };
160
+
161
+ private get _progressPct(): number {
162
+ if (this._dragPct != null) return this._dragPct * 100;
163
+ const d = this.duration;
164
+ if (!isFinite(d) || d <= 0) return 0;
165
+ return Math.min(100, (this.currentTime / d) * 100);
166
+ }
167
+
168
+ protected render() {
169
+ const progressPct = this._progressPct;
170
+ const thumbPct = this._dragPct != null ? this._dragPct * 100 : progressPct;
171
+ const showTooltip = this._hovering || this._dragging;
172
+ const tooltipPct = this._dragging ? (this._dragPct ?? 0) * 100 : this._hoverPct * 100;
173
+ const tooltipTime = this._timeFromPct(tooltipPct / 100);
174
+
175
+ let tooltipText = this._formatTime(tooltipTime);
176
+ if (this.isLive && isFinite(this.duration) && this.duration > 0) {
177
+ const behindLive = tooltipTime - this.duration;
178
+ if (behindLive < -1) tooltipText = this._formatTime(behindLive);
179
+ }
180
+
181
+ return html`
182
+ <div
183
+ class=${classMap({ wrap: true, "wrap--disabled": this.disabled })}
184
+ @pointerdown=${this._handlePointerDown}
185
+ @pointermove=${this._handlePointerMove}
186
+ @pointerup=${this._handlePointerUp}
187
+ @click=${this._handleClick}
188
+ @mouseenter=${this._handleMouseEnter}
189
+ @mouseleave=${this._handleMouseLeave}
190
+ >
191
+ <div
192
+ class=${classMap({ track: true, "track--active": this._dragging, "fw-seek-track": true })}
193
+ >
194
+ <div
195
+ class="progress fw-seek-progress"
196
+ style=${styleMap({ width: `${progressPct}%` })}
197
+ ></div>
198
+ </div>
199
+ <div
200
+ class=${classMap({ thumb: true, "thumb--active": this._dragging, "fw-seek-thumb": true })}
201
+ style=${styleMap({ left: `${thumbPct}%` })}
202
+ ></div>
203
+ ${showTooltip
204
+ ? html`
205
+ <div class="tooltip fw-seek-tooltip" style=${styleMap({ left: `${tooltipPct}%` })}>
206
+ ${tooltipText}
207
+ </div>
208
+ `
209
+ : nothing}
210
+ </div>
211
+ `;
212
+ }
213
+ }
214
+
215
+ declare global {
216
+ interface HTMLElementTagNameMap {
217
+ "fw-seek-bar": FwSeekBar;
218
+ }
219
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * <fw-settings-menu> — Quality, speed, and captions settings popup.
3
+ */
4
+ import { LitElement, html, css, nothing } from "lit";
5
+ import { customElement, property, state } from "lit/decorators.js";
6
+ import { classMap } from "lit/directives/class-map.js";
7
+ import { sharedStyles } from "../styles/shared-styles.js";
8
+ import { utilityStyles } from "../styles/utility-styles.js";
9
+ import type { PlayerControllerHost } from "../controllers/player-controller-host.js";
10
+
11
+ @customElement("fw-settings-menu")
12
+ export class FwSettingsMenu extends LitElement {
13
+ @property({ attribute: false }) pc!: PlayerControllerHost;
14
+ @property({ type: Boolean }) open = false;
15
+
16
+ static styles = [
17
+ sharedStyles,
18
+ utilityStyles,
19
+ css`
20
+ :host {
21
+ display: contents;
22
+ }
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
+ `,
82
+ ];
83
+
84
+ 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];
88
+
89
+ return html`
90
+ <div class="menu fw-settings-menu">
91
+ ${qualities.length > 0
92
+ ? 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
+ )}
106
+ </div>
107
+ `
108
+ : 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>
116
+ `
117
+ )}
118
+ </div>
119
+ </div>
120
+ `;
121
+ }
122
+ }
123
+
124
+ declare global {
125
+ interface HTMLElementTagNameMap {
126
+ "fw-settings-menu": FwSettingsMenu;
127
+ }
128
+ }
@@ -0,0 +1,139 @@
1
+ import { LitElement, html, css, nothing } from "lit";
2
+ import { customElement, property, state } from "lit/decorators.js";
3
+ import { classMap } from "lit/directives/class-map.js";
4
+
5
+ export type SkipDirection = "back" | "forward" | null;
6
+
7
+ @customElement("fw-skip-indicator")
8
+ export class FwSkipIndicator extends LitElement {
9
+ @property({ attribute: false }) direction: SkipDirection = null;
10
+ @property({ type: Number }) seconds = 10;
11
+
12
+ @state() private _visible = false;
13
+ private _timer?: ReturnType<typeof setTimeout>;
14
+
15
+ static styles = css`
16
+ :host {
17
+ display: contents;
18
+ }
19
+ .overlay {
20
+ position: absolute;
21
+ inset: 0;
22
+ pointer-events: none;
23
+ z-index: 25;
24
+ }
25
+ .ripple {
26
+ position: absolute;
27
+ top: 0;
28
+ bottom: 0;
29
+ width: 33%;
30
+ background: rgb(255 255 255 / 0.1);
31
+ border-radius: 50%;
32
+ animation: _fw-ripple 600ms ease-out forwards;
33
+ }
34
+ .ripple--back {
35
+ left: 0;
36
+ border-radius: 0 50% 50% 0;
37
+ }
38
+ .ripple--forward {
39
+ right: 0;
40
+ border-radius: 50% 0 0 50%;
41
+ }
42
+ @keyframes _fw-ripple {
43
+ from {
44
+ opacity: 1;
45
+ }
46
+ to {
47
+ opacity: 0;
48
+ }
49
+ }
50
+ .content {
51
+ position: absolute;
52
+ top: 50%;
53
+ transform: translateY(-50%);
54
+ display: flex;
55
+ flex-direction: column;
56
+ align-items: center;
57
+ gap: 0.25rem;
58
+ color: white;
59
+ animation: _fw-skip-pop 400ms ease-out;
60
+ }
61
+ .content--back {
62
+ left: 12%;
63
+ }
64
+ .content--forward {
65
+ right: 12%;
66
+ }
67
+ @keyframes _fw-skip-pop {
68
+ from {
69
+ opacity: 0;
70
+ transform: translateY(-50%) scale(0.8);
71
+ }
72
+ to {
73
+ opacity: 1;
74
+ transform: translateY(-50%) scale(1);
75
+ }
76
+ }
77
+ .icons {
78
+ display: flex;
79
+ }
80
+ .icons svg:nth-child(2) {
81
+ margin-left: -1rem;
82
+ }
83
+ .time {
84
+ font-size: 0.75rem;
85
+ font-weight: 600;
86
+ }
87
+ `;
88
+
89
+ protected updated() {
90
+ if (this.direction && !this._visible) {
91
+ this._visible = true;
92
+ clearTimeout(this._timer);
93
+ this._timer = setTimeout(() => {
94
+ this._visible = false;
95
+ this.dispatchEvent(new CustomEvent("fw-hide", { bubbles: true, composed: true }));
96
+ }, 600);
97
+ }
98
+ }
99
+
100
+ private _renderRewind() {
101
+ return html`<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
102
+ <path d="M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z" />
103
+ </svg>`;
104
+ }
105
+
106
+ private _renderForward() {
107
+ return html`<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
108
+ <path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z" />
109
+ </svg>`;
110
+ }
111
+
112
+ protected render() {
113
+ if (!this._visible || !this.direction) return nothing;
114
+ const isBack = this.direction === "back";
115
+ return html`
116
+ <div class="overlay">
117
+ <div
118
+ class=${classMap({ ripple: true, "ripple--back": isBack, "ripple--forward": !isBack })}
119
+ ></div>
120
+ <div
121
+ class=${classMap({ content: true, "content--back": isBack, "content--forward": !isBack })}
122
+ >
123
+ <div class="icons">
124
+ ${isBack
125
+ ? html`${this._renderRewind()}${this._renderRewind()}`
126
+ : html`${this._renderForward()}${this._renderForward()}`}
127
+ </div>
128
+ <span class="time">${isBack ? `-${this.seconds}s` : `+${this.seconds}s`}</span>
129
+ </div>
130
+ </div>
131
+ `;
132
+ }
133
+ }
134
+
135
+ declare global {
136
+ interface HTMLElementTagNameMap {
137
+ "fw-skip-indicator": FwSkipIndicator;
138
+ }
139
+ }
@@ -0,0 +1,57 @@
1
+ import { LitElement, html, css } from "lit";
2
+ import { customElement, property } from "lit/decorators.js";
3
+
4
+ @customElement("fw-speed-indicator")
5
+ export class FwSpeedIndicator extends LitElement {
6
+ @property({ type: Number }) speed = 2;
7
+
8
+ static styles = css`
9
+ :host {
10
+ display: contents;
11
+ }
12
+ .pill {
13
+ position: absolute;
14
+ top: 0.75rem;
15
+ right: 0.75rem;
16
+ display: inline-flex;
17
+ align-items: center;
18
+ gap: 0.375rem;
19
+ padding: 0.25rem 0.625rem;
20
+ border-radius: 9999px;
21
+ background: rgb(0 0 0 / 0.6);
22
+ color: white;
23
+ font-size: 0.75rem;
24
+ font-variant-numeric: tabular-nums;
25
+ z-index: 30;
26
+ transform: scale(1);
27
+ animation: _fw-pop 150ms ease-out;
28
+ }
29
+ @keyframes _fw-pop {
30
+ from {
31
+ transform: scale(0.9);
32
+ opacity: 0;
33
+ }
34
+ to {
35
+ transform: scale(1);
36
+ opacity: 1;
37
+ }
38
+ }
39
+ `;
40
+
41
+ protected render() {
42
+ return html`
43
+ <div class="pill fw-speed-indicator">
44
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
45
+ <path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z" />
46
+ </svg>
47
+ <span>${this.speed}x</span>
48
+ </div>
49
+ `;
50
+ }
51
+ }
52
+
53
+ declare global {
54
+ interface HTMLElementTagNameMap {
55
+ "fw-speed-indicator": FwSpeedIndicator;
56
+ }
57
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * <fw-stats-panel> — Stats for nerds overlay.
3
+ * Port of StatsPanel.tsx from player-react.
4
+ */
5
+ import { LitElement, html, css, nothing } from "lit";
6
+ import { customElement, property } from "lit/decorators.js";
7
+ import { sharedStyles } from "../styles/shared-styles.js";
8
+ import { utilityStyles } from "../styles/utility-styles.js";
9
+ import { closeIcon } from "../icons/index.js";
10
+ import type { PlayerControllerHost } from "../controllers/player-controller-host.js";
11
+
12
+ @customElement("fw-stats-panel")
13
+ export class FwStatsPanel extends LitElement {
14
+ @property({ attribute: false }) pc!: PlayerControllerHost;
15
+
16
+ static styles = [
17
+ sharedStyles,
18
+ utilityStyles,
19
+ css`
20
+ :host {
21
+ display: contents;
22
+ }
23
+ .panel {
24
+ position: absolute;
25
+ top: 0.75rem;
26
+ left: 0.75rem;
27
+ z-index: 30;
28
+ min-width: 240px;
29
+ max-width: 320px;
30
+ 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);
39
+ }
40
+ .header {
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: space-between;
44
+ margin-bottom: 0.5rem;
45
+ }
46
+ .title {
47
+ font-size: 0.75rem;
48
+ font-weight: 600;
49
+ color: white;
50
+ }
51
+ .close {
52
+ display: flex;
53
+ background: none;
54
+ border: none;
55
+ color: rgb(255 255 255 / 0.5);
56
+ cursor: pointer;
57
+ padding: 0;
58
+ }
59
+ .close:hover {
60
+ color: white;
61
+ }
62
+ .row {
63
+ display: flex;
64
+ justify-content: space-between;
65
+ padding: 0.125rem 0;
66
+ }
67
+ .label {
68
+ color: rgb(255 255 255 / 0.5);
69
+ }
70
+ .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;
79
+ }
80
+ `,
81
+ ];
82
+
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}`;
87
+ }
88
+
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>`;
94
+ }
95
+
96
+ protected render() {
97
+ const s = this.pc.s;
98
+ const q = s.playbackQuality;
99
+ const meta = s.metadata;
100
+ const ss = s.streamState;
101
+
102
+ return html`
103
+ <div class="panel fw-stats-panel">
104
+ <div class="header">
105
+ <span class="title">Stats</span>
106
+ <button
107
+ class="close"
108
+ @click=${() =>
109
+ this.dispatchEvent(new CustomEvent("fw-close", { bubbles: true, composed: true }))}
110
+ aria-label="Close stats"
111
+ >
112
+ ${closeIcon()}
113
+ </button>
114
+ </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}
145
+ </div>
146
+ `;
147
+ }
148
+ }
149
+
150
+ declare global {
151
+ interface HTMLElementTagNameMap {
152
+ "fw-stats-panel": FwStatsPanel;
153
+ }
154
+ }