@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,557 @@
1
+ /**
2
+ * <fw-player> — Main player web component.
3
+ * Port of Player.tsx / PlayerInner from player-react.
4
+ */
5
+ import { LitElement, html, css, nothing, type PropertyValues } from "lit";
6
+ import { customElement, property, state, query } from "lit/decorators.js";
7
+ import { classMap } from "lit/directives/class-map.js";
8
+ import { PlayerControllerHost } from "../controllers/player-controller-host.js";
9
+ import { sharedStyles } from "../styles/shared-styles.js";
10
+ import { utilityStyles } from "../styles/utility-styles.js";
11
+ import {
12
+ closeIcon,
13
+ statsIcon,
14
+ settingsIcon,
15
+ pictureInPictureIcon,
16
+ loopIcon,
17
+ } from "../icons/index.js";
18
+ import type { ContentEndpoints, PlaybackMode } from "@livepeer-frameworks/player-core";
19
+
20
+ @customElement("fw-player")
21
+ export class FwPlayer extends LitElement {
22
+ // ---- Public attributes (reflected) ----
23
+ @property({ attribute: "content-id" }) contentId = "";
24
+ @property({ attribute: "content-type" }) contentType?: "live" | "dvr" | "clip" | "vod";
25
+ @property({ attribute: "gateway-url" }) gatewayUrl?: string;
26
+ @property({ attribute: "mist-url" }) mistUrl?: string;
27
+ @property({ attribute: "auth-token" }) authToken?: string;
28
+ @property({ type: Boolean }) autoplay = true;
29
+ @property({ type: Boolean }) muted = true;
30
+ @property({ type: Boolean }) controls = false;
31
+ @property({ type: Boolean }) debug = false;
32
+ @property({ type: Boolean, attribute: "dev-mode" }) devMode = false;
33
+ @property({ attribute: "thumbnail-url" }) thumbnailUrl?: string;
34
+ @property({ attribute: "playback-mode" }) playbackMode: PlaybackMode = "auto";
35
+
36
+ // ---- JS-only properties (not reflected) ----
37
+ @property({ attribute: false }) endpoints?: ContentEndpoints;
38
+
39
+ // ---- Internal state ----
40
+ @state() private _isStatsOpen = false;
41
+ @state() private _isDevPanelOpen = false;
42
+ @state() private _skipDirection: "back" | "forward" | null = null;
43
+ @state() private _contextMenuOpen = false;
44
+ @state() private _contextMenuX = 0;
45
+ @state() private _contextMenuY = 0;
46
+
47
+ // ---- Refs ----
48
+ @query("#container") private _containerEl!: HTMLDivElement;
49
+
50
+ // ---- Controller ----
51
+ pc = new PlayerControllerHost(this);
52
+
53
+ static styles = [
54
+ sharedStyles,
55
+ utilityStyles,
56
+ css`
57
+ :host {
58
+ display: block;
59
+ position: relative;
60
+ width: 100%;
61
+ height: 100%;
62
+ contain: layout style;
63
+ }
64
+ :host([hidden]) {
65
+ display: none;
66
+ }
67
+ .player-area {
68
+ position: relative;
69
+ width: 100%;
70
+ height: 100%;
71
+ }
72
+ .player-area--dev {
73
+ flex: 1;
74
+ min-width: 0;
75
+ }
76
+ .context-menu {
77
+ position: fixed;
78
+ z-index: 200;
79
+ min-width: 180px;
80
+ border-radius: 0.5rem;
81
+ border: 1px solid rgb(255 255 255 / 0.1);
82
+ background: rgb(0 0 0 / 0.9);
83
+ backdrop-filter: blur(8px);
84
+ padding: 0.25rem;
85
+ box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.3);
86
+ }
87
+ .context-menu-item {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 0.5rem;
91
+ width: 100%;
92
+ padding: 0.375rem 0.5rem;
93
+ border: none;
94
+ background: none;
95
+ color: rgb(255 255 255 / 0.8);
96
+ font-size: 0.8125rem;
97
+ cursor: pointer;
98
+ border-radius: 0.25rem;
99
+ text-align: left;
100
+ }
101
+ .context-menu-item:hover {
102
+ background: rgb(255 255 255 / 0.1);
103
+ color: white;
104
+ }
105
+ .context-menu-sep {
106
+ height: 1px;
107
+ background: rgb(255 255 255 / 0.1);
108
+ margin: 0.25rem 0;
109
+ }
110
+ `,
111
+ ];
112
+
113
+ // ---- Lifecycle ----
114
+
115
+ protected willUpdate(changed: PropertyValues) {
116
+ if (
117
+ changed.has("contentId") ||
118
+ changed.has("contentType") ||
119
+ changed.has("gatewayUrl") ||
120
+ changed.has("mistUrl") ||
121
+ changed.has("authToken") ||
122
+ changed.has("autoplay") ||
123
+ changed.has("muted") ||
124
+ changed.has("controls") ||
125
+ changed.has("debug") ||
126
+ changed.has("thumbnailUrl") ||
127
+ changed.has("endpoints")
128
+ ) {
129
+ this.pc.configure({
130
+ contentId: this.contentId,
131
+ contentType: this.contentType,
132
+ endpoints: this.endpoints,
133
+ gatewayUrl: this.gatewayUrl,
134
+ mistUrl: this.mistUrl,
135
+ authToken: this.authToken,
136
+ autoplay: this.autoplay,
137
+ muted: this.muted,
138
+ controls: this.controls,
139
+ poster: this.thumbnailUrl,
140
+ debug: this.debug,
141
+ });
142
+ }
143
+ }
144
+
145
+ protected firstUpdated() {
146
+ this.pc.attach(this._containerEl);
147
+
148
+ // Close context menu on outside click
149
+ document.addEventListener("pointerdown", this._handleDocumentClick);
150
+ document.addEventListener("contextmenu", this._handleDocumentContextMenu);
151
+ }
152
+
153
+ disconnectedCallback() {
154
+ super.disconnectedCallback();
155
+ document.removeEventListener("pointerdown", this._handleDocumentClick);
156
+ document.removeEventListener("contextmenu", this._handleDocumentContextMenu);
157
+ }
158
+
159
+ // ---- Context Menu ----
160
+
161
+ private _handleContextMenu = (e: MouseEvent) => {
162
+ e.preventDefault();
163
+ const rect = this.getBoundingClientRect();
164
+ this._contextMenuX = e.clientX;
165
+ this._contextMenuY = e.clientY;
166
+ this._contextMenuOpen = true;
167
+ };
168
+
169
+ private _handleDocumentClick = () => {
170
+ if (this._contextMenuOpen) this._contextMenuOpen = false;
171
+ };
172
+
173
+ private _handleDocumentContextMenu = (e: MouseEvent) => {
174
+ if (!this.contains(e.target as Node)) {
175
+ this._contextMenuOpen = false;
176
+ }
177
+ };
178
+
179
+ // ---- Toast auto-dismiss ----
180
+
181
+ private _toastTimer?: ReturnType<typeof setTimeout>;
182
+
183
+ protected updated(changed: PropertyValues) {
184
+ if (this.pc.s.toast) {
185
+ clearTimeout(this._toastTimer);
186
+ this._toastTimer = setTimeout(() => this.pc.dismissToast(), 3000);
187
+ }
188
+ }
189
+
190
+ // ---- Derived state ----
191
+
192
+ private get _showTitleOverlay() {
193
+ const s = this.pc.s;
194
+ return (s.isHovering || s.isPaused) && !s.shouldShowIdleScreen && !s.isBuffering && !s.error;
195
+ }
196
+
197
+ private get _showBufferingSpinner() {
198
+ const s = this.pc.s;
199
+ return !s.shouldShowIdleScreen && s.isBuffering && !s.error && s.hasPlaybackStarted;
200
+ }
201
+
202
+ private get _showWaitingForEndpoint() {
203
+ const s = this.pc.s;
204
+ return !s.endpoints && s.state !== "booting";
205
+ }
206
+
207
+ private get _useStockControls() {
208
+ return this.controls || this.pc.s.currentPlayerInfo?.shortname === "mist-legacy";
209
+ }
210
+
211
+ // ---- Public API methods ----
212
+
213
+ async play() {
214
+ await this.pc.play();
215
+ }
216
+ pause() {
217
+ this.pc.pause();
218
+ }
219
+ togglePlay() {
220
+ this.pc.togglePlay();
221
+ }
222
+ seek(time: number) {
223
+ this.pc.seek(time);
224
+ }
225
+ seekBy(delta: number) {
226
+ this.pc.seekBy(delta);
227
+ }
228
+ jumpToLive() {
229
+ this.pc.jumpToLive();
230
+ }
231
+ setVolume(volume: number) {
232
+ this.pc.setVolume(volume);
233
+ }
234
+ toggleMute() {
235
+ this.pc.toggleMute();
236
+ }
237
+ toggleLoop() {
238
+ this.pc.toggleLoop();
239
+ }
240
+ async toggleFullscreen() {
241
+ await this.pc.toggleFullscreen();
242
+ }
243
+ async togglePiP() {
244
+ await this.pc.togglePiP();
245
+ }
246
+ toggleSubtitles() {
247
+ this.pc.toggleSubtitles();
248
+ }
249
+ async retry() {
250
+ await this.pc.retry();
251
+ }
252
+ async reload() {
253
+ await this.pc.reload();
254
+ }
255
+ getQualities() {
256
+ return this.pc.getQualities();
257
+ }
258
+ selectQuality(id: string) {
259
+ this.pc.selectQuality(id);
260
+ }
261
+ destroy() {
262
+ this.pc.hostDisconnected();
263
+ }
264
+
265
+ // ---- Render ----
266
+
267
+ protected render() {
268
+ const s = this.pc.s;
269
+
270
+ return html`
271
+ <div
272
+ part="root"
273
+ class=${classMap({
274
+ "fw-player-surface": true,
275
+ "fw-player-root": true,
276
+ "w-full": true,
277
+ "h-full": true,
278
+ "overflow-hidden": true,
279
+ flex: this.devMode,
280
+ })}
281
+ tabindex="0"
282
+ @mouseenter=${() => this.pc.handleMouseEnter()}
283
+ @mouseleave=${() => this.pc.handleMouseLeave()}
284
+ @mousemove=${() => this.pc.handleMouseMove()}
285
+ @touchstart=${() => this.pc.handleTouchStart()}
286
+ @contextmenu=${this._handleContextMenu}
287
+ >
288
+ <!-- Player area -->
289
+ <div
290
+ class=${classMap({
291
+ "player-area": true,
292
+ "player-area--dev": this.devMode,
293
+ })}
294
+ >
295
+ <!-- Video container -->
296
+ <div id="container" part="video-container" class="fw-player-container"></div>
297
+
298
+ <!-- Title overlay -->
299
+ ${this._showTitleOverlay
300
+ ? html`
301
+ <fw-title-overlay
302
+ .title=${s.metadata?.title ?? null}
303
+ .description=${s.metadata?.description ?? null}
304
+ ></fw-title-overlay>
305
+ `
306
+ : nothing}
307
+
308
+ <!-- Stats panel -->
309
+ ${this._isStatsOpen
310
+ ? html`
311
+ <fw-stats-panel
312
+ part="stats-panel"
313
+ .pc=${this.pc}
314
+ @fw-close=${() => {
315
+ this._isStatsOpen = false;
316
+ }}
317
+ ></fw-stats-panel>
318
+ `
319
+ : nothing}
320
+
321
+ <!-- Speed indicator -->
322
+ ${s.isHoldingSpeed
323
+ ? html` <fw-speed-indicator .speed=${s.holdSpeed}></fw-speed-indicator> `
324
+ : nothing}
325
+
326
+ <!-- Skip indicator -->
327
+ <fw-skip-indicator
328
+ .direction=${this._skipDirection}
329
+ @fw-hide=${() => {
330
+ this._skipDirection = null;
331
+ }}
332
+ ></fw-skip-indicator>
333
+
334
+ <!-- Waiting for endpoint -->
335
+ ${this._showWaitingForEndpoint
336
+ ? html`
337
+ <fw-idle-screen status="OFFLINE" message="Waiting for endpoint..."></fw-idle-screen>
338
+ `
339
+ : nothing}
340
+
341
+ <!-- Idle screen -->
342
+ ${!this._showWaitingForEndpoint && s.shouldShowIdleScreen
343
+ ? html`
344
+ <fw-idle-screen
345
+ .status=${s.isEffectivelyLive ? s.streamState?.status : undefined}
346
+ .message=${s.isEffectivelyLive ? s.streamState?.message : "Loading video..."}
347
+ .percentage=${s.isEffectivelyLive ? s.streamState?.percentage : undefined}
348
+ ></fw-idle-screen>
349
+ `
350
+ : nothing}
351
+
352
+ <!-- Buffering spinner -->
353
+ ${this._showBufferingSpinner
354
+ ? html`
355
+ <div
356
+ role="status"
357
+ aria-live="polite"
358
+ class="fw-player-surface absolute inset-0 flex items-center justify-center bg-black/40 backdrop-blur-sm z-20"
359
+ >
360
+ <div
361
+ class="flex items-center gap-3 rounded-lg border border-white/10 bg-black/70 px-4 py-3 text-sm text-white shadow-lg"
362
+ >
363
+ <div
364
+ class="w-4 h-4 border-2 border-white/10 rounded-full animate-spin"
365
+ style="border-top-color: white;"
366
+ ></div>
367
+ <span>Buffering...</span>
368
+ </div>
369
+ </div>
370
+ `
371
+ : nothing}
372
+
373
+ <!-- Error overlay -->
374
+ ${!s.shouldShowIdleScreen && s.error
375
+ ? html`
376
+ <div
377
+ role="alert"
378
+ aria-live="assertive"
379
+ class=${classMap({
380
+ "fw-error-overlay": true,
381
+ "fw-error-overlay--passive": s.isPassiveError,
382
+ "fw-error-overlay--fullscreen": !s.isPassiveError,
383
+ })}
384
+ >
385
+ <div
386
+ class=${classMap({
387
+ "fw-error-popup": true,
388
+ "fw-error-popup--passive": s.isPassiveError,
389
+ "fw-error-popup--fullscreen": !s.isPassiveError,
390
+ })}
391
+ >
392
+ <div
393
+ class=${classMap({
394
+ "fw-error-header": true,
395
+ "fw-error-header--warning": s.isPassiveError,
396
+ "fw-error-header--error": !s.isPassiveError,
397
+ })}
398
+ >
399
+ <span
400
+ class=${classMap({
401
+ "fw-error-title": true,
402
+ "fw-error-title--warning": s.isPassiveError,
403
+ "fw-error-title--error": !s.isPassiveError,
404
+ })}
405
+ >${s.isPassiveError ? "Warning" : "Error"}</span
406
+ >
407
+ <button
408
+ type="button"
409
+ class="fw-error-close"
410
+ @click=${() => this.pc.clearError()}
411
+ aria-label="Dismiss"
412
+ >
413
+ ${closeIcon()}
414
+ </button>
415
+ </div>
416
+ <div class="fw-error-body">
417
+ <p class="fw-error-message">Playback issue</p>
418
+ </div>
419
+ <div class="fw-error-actions">
420
+ <button
421
+ type="button"
422
+ class="fw-error-btn"
423
+ aria-label="Retry playback"
424
+ @click=${() => {
425
+ this.pc.clearError();
426
+ this.pc.retry();
427
+ }}
428
+ >
429
+ Retry
430
+ </button>
431
+ </div>
432
+ </div>
433
+ </div>
434
+ `
435
+ : nothing}
436
+
437
+ <!-- Toast notification -->
438
+ ${s.toast
439
+ ? html`
440
+ <div
441
+ class="absolute bottom-20 left-1/2 -translate-x-1/2 z-30"
442
+ role="status"
443
+ aria-live="polite"
444
+ >
445
+ <div
446
+ class="flex items-center gap-2 rounded-lg border border-white/10 bg-black/80 px-4 py-2 text-sm text-white shadow-lg backdrop-blur-sm"
447
+ >
448
+ <span>${s.toast.message}</span>
449
+ <button
450
+ type="button"
451
+ @click=${() => this.pc.dismissToast()}
452
+ class="ml-0.5 text-white/60 hover\\:text-white cursor-pointer"
453
+ aria-label="Dismiss"
454
+ >
455
+ ${closeIcon()}
456
+ </button>
457
+ </div>
458
+ </div>
459
+ `
460
+ : nothing}
461
+
462
+ <!-- Player controls -->
463
+ ${!this._useStockControls
464
+ ? html`
465
+ <fw-player-controls
466
+ part="controls"
467
+ .pc=${this.pc}
468
+ .playbackMode=${this.playbackMode}
469
+ .isContentLive=${s.isEffectivelyLive}
470
+ .isStatsOpen=${this._isStatsOpen}
471
+ @fw-stats-toggle=${() => {
472
+ this._isStatsOpen = !this._isStatsOpen;
473
+ }}
474
+ ></fw-player-controls>
475
+ `
476
+ : nothing}
477
+ </div>
478
+
479
+ <!-- Dev mode side panel -->
480
+ ${this.devMode && this._isDevPanelOpen
481
+ ? html`
482
+ <fw-dev-mode-panel
483
+ .pc=${this.pc}
484
+ .playbackMode=${this.playbackMode}
485
+ @fw-close=${() => {
486
+ this._isDevPanelOpen = false;
487
+ }}
488
+ ></fw-dev-mode-panel>
489
+ `
490
+ : nothing}
491
+ </div>
492
+
493
+ <!-- Context menu -->
494
+ ${this._contextMenuOpen
495
+ ? html`
496
+ <div
497
+ class="context-menu"
498
+ style="left: ${this._contextMenuX}px; top: ${this._contextMenuY}px;"
499
+ >
500
+ <button
501
+ class="context-menu-item"
502
+ @click=${() => {
503
+ this._isStatsOpen = !this._isStatsOpen;
504
+ this._contextMenuOpen = false;
505
+ }}
506
+ >
507
+ <span class="opacity-70 shrink-0">${statsIcon(14)}</span>
508
+ <span>${this._isStatsOpen ? "Hide Stats" : "Stats"}</span>
509
+ </button>
510
+ ${this.devMode
511
+ ? html`
512
+ <div class="context-menu-sep"></div>
513
+ <button
514
+ class="context-menu-item"
515
+ @click=${() => {
516
+ this._isDevPanelOpen = !this._isDevPanelOpen;
517
+ this._contextMenuOpen = false;
518
+ }}
519
+ >
520
+ <span class="opacity-70 shrink-0">${settingsIcon(14)}</span>
521
+ <span>${this._isDevPanelOpen ? "Hide Settings" : "Settings"}</span>
522
+ </button>
523
+ `
524
+ : nothing}
525
+ <div class="context-menu-sep"></div>
526
+ <button
527
+ class="context-menu-item"
528
+ @click=${() => {
529
+ this.pc.togglePiP();
530
+ this._contextMenuOpen = false;
531
+ }}
532
+ >
533
+ <span class="opacity-70 shrink-0">${pictureInPictureIcon(14)}</span>
534
+ <span>Picture-in-Picture</span>
535
+ </button>
536
+ <button
537
+ class="context-menu-item"
538
+ @click=${() => {
539
+ this.pc.toggleLoop();
540
+ this._contextMenuOpen = false;
541
+ }}
542
+ >
543
+ <span class="opacity-70 shrink-0">${loopIcon(14)}</span>
544
+ <span>${s.isLoopEnabled ? "Disable Loop" : "Enable Loop"}</span>
545
+ </button>
546
+ </div>
547
+ `
548
+ : nothing}
549
+ `;
550
+ }
551
+ }
552
+
553
+ declare global {
554
+ interface HTMLElementTagNameMap {
555
+ "fw-player": FwPlayer;
556
+ }
557
+ }