@livepeer-frameworks/player-wc 0.1.9 → 0.2.1

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 (140) hide show
  1. package/dist/esm/components/controls/fw-fullscreen-button.js +76 -0
  2. package/dist/esm/components/controls/fw-fullscreen-button.js.map +1 -0
  3. package/dist/esm/components/controls/fw-live-badge.js +109 -0
  4. package/dist/esm/components/controls/fw-live-badge.js.map +1 -0
  5. package/dist/esm/components/controls/fw-play-button.js +76 -0
  6. package/dist/esm/components/controls/fw-play-button.js.map +1 -0
  7. package/dist/esm/components/controls/fw-skip-button.js +62 -0
  8. package/dist/esm/components/controls/fw-skip-button.js.map +1 -0
  9. package/dist/esm/components/controls/fw-time-display.js +77 -0
  10. package/dist/esm/components/controls/fw-time-display.js.map +1 -0
  11. package/dist/esm/components/controls/fw-volume-control.js +76 -0
  12. package/dist/esm/components/controls/fw-volume-control.js.map +1 -0
  13. package/dist/esm/components/fw-dev-mode-panel.js +11 -15
  14. package/dist/esm/components/fw-dev-mode-panel.js.map +1 -1
  15. package/dist/esm/components/fw-error-overlay.js +13 -5
  16. package/dist/esm/components/fw-error-overlay.js.map +1 -1
  17. package/dist/esm/components/fw-idle-screen.js +10 -2
  18. package/dist/esm/components/fw-idle-screen.js.map +1 -1
  19. package/dist/esm/components/fw-loading-screen.js +89 -42
  20. package/dist/esm/components/fw-loading-screen.js.map +1 -1
  21. package/dist/esm/components/fw-loading-spinner.js +20 -9
  22. package/dist/esm/components/fw-loading-spinner.js.map +1 -1
  23. package/dist/esm/components/fw-player-controls.js +18 -13
  24. package/dist/esm/components/fw-player-controls.js.map +1 -1
  25. package/dist/esm/components/fw-player.js +165 -59
  26. package/dist/esm/components/fw-player.js.map +1 -1
  27. package/dist/esm/components/fw-settings-menu.js +44 -9
  28. package/dist/esm/components/fw-settings-menu.js.map +1 -1
  29. package/dist/esm/components/fw-stream-state-overlay.js +13 -5
  30. package/dist/esm/components/fw-stream-state-overlay.js.map +1 -1
  31. package/dist/esm/components/fw-toast.js +11 -1
  32. package/dist/esm/components/fw-toast.js.map +1 -1
  33. package/dist/esm/components/fw-volume-control.js +13 -3
  34. package/dist/esm/components/fw-volume-control.js.map +1 -1
  35. package/dist/esm/controllers/player-controller-host.js +14 -1
  36. package/dist/esm/controllers/player-controller-host.js.map +1 -1
  37. package/dist/esm/index.js +6 -0
  38. package/dist/esm/index.js.map +1 -1
  39. package/dist/esm/styles/shared-styles.js +401 -304
  40. package/dist/esm/styles/shared-styles.js.map +1 -1
  41. package/dist/fw-player.iife.js +707 -488
  42. package/dist/types/components/controls/fw-fullscreen-button.d.ts +18 -0
  43. package/dist/types/components/controls/fw-live-badge.d.ts +19 -0
  44. package/dist/types/components/controls/fw-play-button.d.ts +18 -0
  45. package/dist/types/components/controls/fw-skip-button.d.ts +17 -0
  46. package/dist/types/components/controls/fw-time-display.d.ts +17 -0
  47. package/dist/types/components/controls/fw-volume-control.d.ts +18 -0
  48. package/dist/types/components/controls/index.d.ts +6 -0
  49. package/dist/types/components/fw-dev-mode-panel.d.ts +1 -1
  50. package/dist/types/components/fw-error-overlay.d.ts +4 -0
  51. package/dist/types/components/fw-idle-screen.d.ts +4 -0
  52. package/dist/types/components/fw-loading-screen.d.ts +5 -1
  53. package/dist/types/components/fw-loading-spinner.d.ts +4 -0
  54. package/dist/types/components/fw-player-controls.d.ts +2 -1
  55. package/dist/types/components/fw-player.d.ts +10 -1
  56. package/dist/types/components/fw-settings-menu.d.ts +3 -1
  57. package/dist/types/components/fw-stream-state-overlay.d.ts +4 -0
  58. package/dist/types/components/fw-toast.d.ts +4 -0
  59. package/dist/types/controllers/player-controller-host.d.ts +7 -1
  60. package/dist/types/index.d.ts +1 -0
  61. package/package.json +10 -13
  62. package/src/components/controls/fw-fullscreen-button.ts +75 -0
  63. package/src/components/controls/fw-live-badge.ts +109 -0
  64. package/src/components/controls/fw-play-button.ts +75 -0
  65. package/src/components/controls/fw-skip-button.ts +59 -0
  66. package/src/components/controls/fw-time-display.ts +74 -0
  67. package/src/components/controls/fw-volume-control.ts +75 -0
  68. package/src/components/controls/index.ts +6 -0
  69. package/src/components/fw-dev-mode-panel.ts +10 -17
  70. package/src/components/fw-error-overlay.ts +13 -5
  71. package/src/components/fw-idle-screen.ts +10 -2
  72. package/src/components/fw-loading-screen.ts +90 -46
  73. package/src/components/fw-loading-spinner.ts +18 -9
  74. package/src/components/fw-player-controls.ts +17 -13
  75. package/src/components/fw-player.ts +166 -64
  76. package/src/components/fw-settings-menu.ts +49 -9
  77. package/src/components/fw-stream-state-overlay.ts +13 -5
  78. package/src/components/fw-toast.ts +11 -1
  79. package/src/components/fw-volume-control.ts +14 -3
  80. package/src/controllers/player-controller-host.ts +18 -0
  81. package/src/index.ts +10 -0
  82. package/src/styles/shared-styles.ts +401 -304
  83. package/dist/cjs/components/fw-context-menu.js +0 -17
  84. package/dist/cjs/components/fw-context-menu.js.map +0 -1
  85. package/dist/cjs/components/fw-dev-mode-panel.js +0 -907
  86. package/dist/cjs/components/fw-dev-mode-panel.js.map +0 -1
  87. package/dist/cjs/components/fw-dvd-logo.js +0 -211
  88. package/dist/cjs/components/fw-dvd-logo.js.map +0 -1
  89. package/dist/cjs/components/fw-error-overlay.js +0 -101
  90. package/dist/cjs/components/fw-error-overlay.js.map +0 -1
  91. package/dist/cjs/components/fw-idle-screen.js +0 -726
  92. package/dist/cjs/components/fw-idle-screen.js.map +0 -1
  93. package/dist/cjs/components/fw-loading-screen.js +0 -513
  94. package/dist/cjs/components/fw-loading-screen.js.map +0 -1
  95. package/dist/cjs/components/fw-loading-spinner.js +0 -62
  96. package/dist/cjs/components/fw-loading-spinner.js.map +0 -1
  97. package/dist/cjs/components/fw-player-controls.js +0 -451
  98. package/dist/cjs/components/fw-player-controls.js.map +0 -1
  99. package/dist/cjs/components/fw-player.js +0 -832
  100. package/dist/cjs/components/fw-player.js.map +0 -1
  101. package/dist/cjs/components/fw-seek-bar.js +0 -383
  102. package/dist/cjs/components/fw-seek-bar.js.map +0 -1
  103. package/dist/cjs/components/fw-settings-menu.js +0 -253
  104. package/dist/cjs/components/fw-settings-menu.js.map +0 -1
  105. package/dist/cjs/components/fw-skip-indicator.js +0 -143
  106. package/dist/cjs/components/fw-skip-indicator.js.map +0 -1
  107. package/dist/cjs/components/fw-speed-indicator.js +0 -61
  108. package/dist/cjs/components/fw-speed-indicator.js.map +0 -1
  109. package/dist/cjs/components/fw-stats-panel.js +0 -205
  110. package/dist/cjs/components/fw-stats-panel.js.map +0 -1
  111. package/dist/cjs/components/fw-stream-state-overlay.js +0 -338
  112. package/dist/cjs/components/fw-stream-state-overlay.js.map +0 -1
  113. package/dist/cjs/components/fw-subtitle-renderer.js +0 -217
  114. package/dist/cjs/components/fw-subtitle-renderer.js.map +0 -1
  115. package/dist/cjs/components/fw-thumbnail-overlay.js +0 -161
  116. package/dist/cjs/components/fw-thumbnail-overlay.js.map +0 -1
  117. package/dist/cjs/components/fw-title-overlay.js +0 -72
  118. package/dist/cjs/components/fw-title-overlay.js.map +0 -1
  119. package/dist/cjs/components/fw-toast.js +0 -74
  120. package/dist/cjs/components/fw-toast.js.map +0 -1
  121. package/dist/cjs/components/fw-volume-control.js +0 -276
  122. package/dist/cjs/components/fw-volume-control.js.map +0 -1
  123. package/dist/cjs/components/shared/hitmarker-audio.js +0 -76
  124. package/dist/cjs/components/shared/hitmarker-audio.js.map +0 -1
  125. package/dist/cjs/constants/media-assets.js +0 -11
  126. package/dist/cjs/constants/media-assets.js.map +0 -1
  127. package/dist/cjs/controllers/player-controller-host.js +0 -364
  128. package/dist/cjs/controllers/player-controller-host.js.map +0 -1
  129. package/dist/cjs/define.js +0 -53
  130. package/dist/cjs/define.js.map +0 -1
  131. package/dist/cjs/icons/index.js +0 -180
  132. package/dist/cjs/icons/index.js.map +0 -1
  133. package/dist/cjs/index.js +0 -108
  134. package/dist/cjs/index.js.map +0 -1
  135. 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 +0 -33
  136. 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 +0 -1
  137. package/dist/cjs/styles/shared-styles.js +0 -1985
  138. package/dist/cjs/styles/shared-styles.js.map +0 -1
  139. package/dist/cjs/styles/utility-styles.js +0 -725
  140. package/dist/cjs/styles/utility-styles.js.map +0 -1
@@ -1,8 +1,17 @@
1
1
  import { LitElement, html, css } from "lit";
2
- import { customElement } from "lit/decorators.js";
2
+ import { customElement, property } from "lit/decorators.js";
3
+ import { createTranslator, type TranslateFn } from "@livepeer-frameworks/player-core";
3
4
 
4
5
  @customElement("fw-loading-spinner")
5
6
  export class FwLoadingSpinner extends LitElement {
7
+ @property({ attribute: false }) translator?: TranslateFn;
8
+
9
+ private _defaultTranslator: TranslateFn = createTranslator({ locale: "en" });
10
+
11
+ private get _t(): TranslateFn {
12
+ return this.translator ?? this._defaultTranslator;
13
+ }
14
+
6
15
  static styles = css`
7
16
  :host {
8
17
  display: contents;
@@ -13,7 +22,7 @@ export class FwLoadingSpinner extends LitElement {
13
22
  display: flex;
14
23
  align-items: center;
15
24
  justify-content: center;
16
- background: rgb(0 0 0 / 0.4);
25
+ background: hsl(var(--fw-surface-deep, 235 21% 11%) / 0.85);
17
26
  backdrop-filter: blur(4px);
18
27
  z-index: 20;
19
28
  }
@@ -22,18 +31,18 @@ export class FwLoadingSpinner extends LitElement {
22
31
  align-items: center;
23
32
  gap: 0.75rem;
24
33
  border-radius: 0.5rem;
25
- border: 1px solid rgb(255 255 255 / 0.1);
26
- background: rgb(0 0 0 / 0.7);
34
+ border: 1px solid hsl(var(--fw-text, 229 73% 86%) / 0.1);
35
+ background: hsl(var(--fw-surface-deep, 235 21% 11%) / 0.9);
27
36
  padding: 0.75rem 1rem;
28
37
  font-size: 0.875rem;
29
- color: white;
30
- box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
38
+ color: hsl(var(--fw-text, 229 73% 86%));
39
+ box-shadow: 0 10px 15px -3px hsl(var(--fw-shadow-color, 0 0% 0%) / 0.1);
31
40
  }
32
41
  .spinner {
33
42
  width: 1rem;
34
43
  height: 1rem;
35
- border: 2px solid rgb(255 255 255 / 0.3);
36
- border-top-color: white;
44
+ border: 2px solid hsl(var(--fw-text-faint, 228 15% 45%) / 0.3);
45
+ border-top-color: hsl(var(--fw-accent, 218 79% 73%));
37
46
  border-radius: 50%;
38
47
  animation: _fw-spin 1s linear infinite;
39
48
  }
@@ -49,7 +58,7 @@ export class FwLoadingSpinner extends LitElement {
49
58
  <div class="overlay" role="status" aria-live="polite">
50
59
  <div class="pill">
51
60
  <div class="spinner"></div>
52
- <span>Buffering...</span>
61
+ <span>${this._t("buffering")}</span>
53
62
  </div>
54
63
  </div>
55
64
  `;
@@ -28,6 +28,7 @@ import {
28
28
  isMediaStreamSource,
29
29
  type MistStreamInfo,
30
30
  type PlaybackMode,
31
+ type FwLocale,
31
32
  } from "@livepeer-frameworks/player-core";
32
33
  import type { PlayerControllerHost } from "../controllers/player-controller-host.js";
33
34
 
@@ -51,6 +52,7 @@ export class FwPlayerControls extends LitElement {
51
52
  @property({ type: Boolean, attribute: "dev-mode" }) devMode = false;
52
53
  @property({ type: Boolean, attribute: "show-stats-button" }) showStatsButton = false;
53
54
  @property({ type: Boolean, attribute: "is-stats-open" }) isStatsOpen = false;
55
+ @property({ attribute: "active-locale" }) activeLocale?: FwLocale;
54
56
 
55
57
  @state() private _settingsOpen = false;
56
58
  @state() private _isNearLiveState = true;
@@ -339,7 +341,6 @@ export class FwPlayerControls extends LitElement {
339
341
  return html`
340
342
  <div
341
343
  class=${classMap({
342
- "fw-player-surface": true,
343
344
  "fw-controls-wrapper": true,
344
345
  "fw-controls-wrapper--visible": shouldShowControls,
345
346
  "fw-controls-wrapper--hidden": !shouldShowControls,
@@ -372,7 +373,7 @@ export class FwPlayerControls extends LitElement {
372
373
  type="button"
373
374
  class="fw-btn-flush"
374
375
  ?disabled=${disabled}
375
- aria-label=${state.isPlaying ? "Pause" : "Play"}
376
+ aria-label=${state.isPlaying ? this.pc.t("pause") : this.pc.t("play")}
376
377
  @click=${() => this.pc.togglePlay()}
377
378
  >
378
379
  ${state.isPlaying ? pauseIcon(18) : playIcon(18)}
@@ -384,7 +385,7 @@ export class FwPlayerControls extends LitElement {
384
385
  type="button"
385
386
  class="fw-btn-flush hidden sm:flex"
386
387
  ?disabled=${disabled}
387
- aria-label="Skip back 10 seconds"
388
+ aria-label=${this.pc.t("skipBackward")}
388
389
  @click=${() => this.pc.seekBy(-10)}
389
390
  >
390
391
  ${skipBackIcon(16)}
@@ -393,7 +394,7 @@ export class FwPlayerControls extends LitElement {
393
394
  type="button"
394
395
  class="fw-btn-flush hidden sm:flex"
395
396
  ?disabled=${disabled}
396
- aria-label="Skip forward 10 seconds"
397
+ aria-label=${this.pc.t("skipForward")}
397
398
  @click=${() => this.pc.seekBy(10)}
398
399
  >
399
400
  ${skipForwardIcon(16)}
@@ -426,12 +427,12 @@ export class FwPlayerControls extends LitElement {
426
427
  "fw-live-badge--behind": !liveButtonDisabled,
427
428
  })}
428
429
  title=${!context.hasDvrWindow
429
- ? "Live only"
430
+ ? this.pc.t("live")
430
431
  : this._isNearLiveState
431
- ? "At live edge"
432
- : "Jump to live"}
432
+ ? this.pc.t("live")
433
+ : this.pc.t("live")}
433
434
  >
434
- LIVE
435
+ ${this.pc.t("live").toUpperCase()}
435
436
  ${!this._isNearLiveState && context.hasDvrWindow
436
437
  ? seekToLiveIcon(10)
437
438
  : nothing}
@@ -451,8 +452,8 @@ export class FwPlayerControls extends LitElement {
451
452
  "fw-btn-flush": true,
452
453
  "fw-btn-flush--active": this.isStatsOpen,
453
454
  })}
454
- aria-label="Toggle stats"
455
- title="Stats"
455
+ aria-label=${this.pc.t("showStats")}
456
+ title=${this.pc.t("showStats")}
456
457
  @click=${() =>
457
458
  this.dispatchEvent(
458
459
  new CustomEvent("fw-stats-toggle", {
@@ -474,8 +475,8 @@ export class FwPlayerControls extends LitElement {
474
475
  group: true,
475
476
  "fw-btn-flush--active": this._settingsOpen,
476
477
  })}
477
- aria-label="Settings"
478
- title="Settings"
478
+ aria-label=${this.pc.t("settings")}
479
+ title=${this.pc.t("settings")}
479
480
  ?disabled=${disabled}
480
481
  @click=${(event: MouseEvent) => {
481
482
  event.stopPropagation();
@@ -495,6 +496,7 @@ export class FwPlayerControls extends LitElement {
495
496
  .open=${this._settingsOpen}
496
497
  .playbackMode=${this.playbackMode}
497
498
  .isContentLive=${this.isContentLive}
499
+ .activeLocale=${this.activeLocale}
498
500
  @click=${(event: MouseEvent) => event.stopPropagation()}
499
501
  @fw-close=${() => {
500
502
  this._settingsOpen = false;
@@ -508,7 +510,9 @@ export class FwPlayerControls extends LitElement {
508
510
  type="button"
509
511
  class="fw-btn-flush"
510
512
  ?disabled=${disabled}
511
- aria-label=${state.isFullscreen ? "Exit fullscreen" : "Fullscreen"}
513
+ aria-label=${state.isFullscreen
514
+ ? this.pc.t("exitFullscreen")
515
+ : this.pc.t("fullscreen")}
512
516
  @click=${() => this.pc.toggleFullscreen()}
513
517
  >
514
518
  ${state.isFullscreen ? fullscreenExitIcon(16) : fullscreenIcon(16)}
@@ -15,7 +15,14 @@ import {
15
15
  pictureInPictureIcon,
16
16
  loopIcon,
17
17
  } from "../icons/index.js";
18
- import type { ContentEndpoints, PlaybackMode } from "@livepeer-frameworks/player-core";
18
+ import type {
19
+ ContentEndpoints,
20
+ PlaybackMode,
21
+ FwThemePreset,
22
+ FwThemeOverrides,
23
+ FwLocale,
24
+ } from "@livepeer-frameworks/player-core";
25
+ import { applyTheme, applyThemeOverrides, clearTheme } from "@livepeer-frameworks/player-core";
19
26
 
20
27
  @customElement("fw-player")
21
28
  export class FwPlayer extends LitElement {
@@ -37,6 +44,11 @@ export class FwPlayer extends LitElement {
37
44
  @property({ attribute: "thumbnail-url" }) thumbnailUrl?: string;
38
45
  @property({ attribute: "playback-mode" }) playbackMode: PlaybackMode = "auto";
39
46
 
47
+ // ---- Theme ----
48
+ @property({ attribute: "theme" }) theme?: FwThemePreset;
49
+ @property({ attribute: false }) themeOverrides?: FwThemeOverrides;
50
+ @property({ attribute: "locale" }) locale?: FwLocale;
51
+
40
52
  // ---- JS-only properties (not reflected) ----
41
53
  @property({ attribute: false }) endpoints?: ContentEndpoints;
42
54
 
@@ -46,6 +58,12 @@ export class FwPlayer extends LitElement {
46
58
  @state() private _skipDirection: "back" | "forward" | null = null;
47
59
  @state() private _contextMenuOpen = false;
48
60
  @state() private _contextMenuMounted = false;
61
+
62
+ // Error fade-out
63
+ @state() private _displayedError: string | null = null;
64
+ @state() private _displayedIsPassive = false;
65
+ @state() private _isErrorDismissing = false;
66
+ private _errorDismissTimer: ReturnType<typeof setTimeout> | null = null;
49
67
  @state() private _contextMenuState: "open" | "closed" = "closed";
50
68
  @state() private _contextMenuSide: "top" | "bottom" | "left" | "right" = "bottom";
51
69
  @state() private _contextMenuX = 0;
@@ -66,6 +84,8 @@ export class FwPlayer extends LitElement {
66
84
  position: relative;
67
85
  width: 100%;
68
86
  height: 100%;
87
+ min-height: 0;
88
+ overflow: hidden;
69
89
  contain: layout style;
70
90
  }
71
91
  :host([hidden]) {
@@ -87,6 +107,30 @@ export class FwPlayer extends LitElement {
87
107
  // ---- Lifecycle ----
88
108
 
89
109
  protected willUpdate(changed: PropertyValues) {
110
+ if (changed.has("locale")) {
111
+ this.pc.updateTranslator({ locale: this.locale ?? "en" });
112
+ }
113
+
114
+ // Error fade-out: sync displayed error from controller state
115
+ const es = this.pc.s;
116
+ if (es.error) {
117
+ if (this._errorDismissTimer) {
118
+ clearTimeout(this._errorDismissTimer);
119
+ this._errorDismissTimer = null;
120
+ }
121
+ this._displayedError = es.error;
122
+ this._displayedIsPassive = es.isPassiveError;
123
+ this._isErrorDismissing = false;
124
+ } else if (this._displayedError && !this._isErrorDismissing) {
125
+ this._isErrorDismissing = true;
126
+ this._errorDismissTimer = setTimeout(() => {
127
+ this._displayedError = null;
128
+ this._displayedIsPassive = false;
129
+ this._isErrorDismissing = false;
130
+ this._errorDismissTimer = null;
131
+ }, 300);
132
+ }
133
+
90
134
  if (
91
135
  changed.has("contentId") ||
92
136
  changed.has("contentType") ||
@@ -135,6 +179,10 @@ export class FwPlayer extends LitElement {
135
179
  clearTimeout(this._contextMenuCloseTimer);
136
180
  this._contextMenuCloseTimer = undefined;
137
181
  }
182
+ if (this._errorDismissTimer) {
183
+ clearTimeout(this._errorDismissTimer);
184
+ this._errorDismissTimer = null;
185
+ }
138
186
  this._resetContextMenuTypeahead();
139
187
  }
140
188
 
@@ -407,6 +455,18 @@ export class FwPlayer extends LitElement {
407
455
  private _toastTimer?: ReturnType<typeof setTimeout>;
408
456
 
409
457
  protected updated(changed: PropertyValues) {
458
+ // Apply theme changes (preset or overrides) via JS custom properties
459
+ if (changed.has("theme") || changed.has("themeOverrides")) {
460
+ const root = this.shadowRoot?.querySelector<HTMLElement>('[part="root"]');
461
+ if (root) {
462
+ clearTheme(root);
463
+ if (this.theme && this.theme !== "default") {
464
+ applyTheme(root, this.theme);
465
+ }
466
+ if (this.themeOverrides) applyThemeOverrides(root, this.themeOverrides);
467
+ }
468
+ }
469
+
410
470
  if (this.pc.s.toast) {
411
471
  clearTimeout(this._toastTimer);
412
472
  this._toastTimer = setTimeout(() => this.pc.dismissToast(), 3000);
@@ -443,9 +503,9 @@ export class FwPlayer extends LitElement {
443
503
  private get _waitingMessage() {
444
504
  const s = this.pc.s;
445
505
  if (this.gatewayUrl && s.state === "gateway_loading") {
446
- return "Resolving viewing endpoint...";
506
+ return this.pc.t("resolvingEndpoint");
447
507
  }
448
- return "Waiting for endpoint...";
508
+ return this.pc.t("waitingForStream");
449
509
  }
450
510
 
451
511
  private get _useStockControls() {
@@ -456,6 +516,11 @@ export class FwPlayer extends LitElement {
456
516
  );
457
517
  }
458
518
 
519
+ /** Expose the PlayerControllerHost for composable controls */
520
+ get controller(): PlayerControllerHost {
521
+ return this.pc;
522
+ }
523
+
459
524
  // ---- Public API methods ----
460
525
 
461
526
  async play() {
@@ -527,6 +592,7 @@ export class FwPlayer extends LitElement {
527
592
  flex: this.devMode,
528
593
  })}
529
594
  data-player-container="true"
595
+ data-theme=${this.theme && this.theme !== "default" ? this.theme : nothing}
530
596
  tabindex="0"
531
597
  @mouseenter=${() => this.pc.handleMouseEnter()}
532
598
  @mouseleave=${() => this.pc.handleMouseLeave()}
@@ -610,7 +676,7 @@ export class FwPlayer extends LitElement {
610
676
  ? html`
611
677
  <fw-idle-screen
612
678
  .status=${s.isEffectivelyLive ? s.streamState?.status : undefined}
613
- .message=${s.isEffectivelyLive ? s.streamState?.message : "Loading video..."}
679
+ .message=${s.isEffectivelyLive ? s.streamState?.message : this.pc.t("loading")}
614
680
  .percentage=${s.isEffectivelyLive ? s.streamState?.percentage : undefined}
615
681
  @fw-retry=${() => {
616
682
  this.pc.clearError();
@@ -622,82 +688,108 @@ export class FwPlayer extends LitElement {
622
688
 
623
689
  <!-- Buffering spinner -->
624
690
  ${this._showBufferingSpinner
691
+ ? html`
692
+ <div role="status" aria-live="polite" class="fw-buffering-overlay">
693
+ <div class="fw-buffering-pill">
694
+ <div class="fw-buffering-spinner"></div>
695
+ <span>${this.pc.t("buffering")}</span>
696
+ </div>
697
+ </div>
698
+ `
699
+ : nothing}
700
+
701
+ <!-- Passive error toast (non-blocking) -->
702
+ ${!s.shouldShowIdleScreen && this._displayedError && this._displayedIsPassive
625
703
  ? html`
626
704
  <div
705
+ class="absolute bottom-20 left-1/2 -translate-x-1/2 z-30"
706
+ style="transition:opacity 300ms;opacity:${this._isErrorDismissing ? "0" : "1"}"
627
707
  role="status"
628
708
  aria-live="polite"
629
- class="fw-player-surface absolute inset-0 flex items-center justify-center bg-black/40 backdrop-blur-sm z-20"
630
709
  >
631
710
  <div
632
- 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"
711
+ class="flex items-center gap-2 rounded-lg border border-yellow-500/30 bg-black/80 px-4 py-2 text-sm text-white shadow-lg backdrop-blur-sm"
633
712
  >
634
- <div
635
- class="w-4 h-4 border-2 border-white/10 rounded-full animate-spin"
636
- style="border-top-color: white;"
637
- ></div>
638
- <span>Buffering...</span>
713
+ <span class="text-yellow-400 text-xs font-semibold uppercase"
714
+ >${this.pc.t("warning")}</span
715
+ >
716
+ <span>${this._displayedError}</span>
717
+ <button
718
+ type="button"
719
+ @click=${() => this.pc.clearError()}
720
+ class="ml-0.5 text-white/60 hover\\:text-white cursor-pointer"
721
+ aria-label=${this.pc.t("dismiss")}
722
+ >
723
+ ${closeIcon()}
724
+ </button>
639
725
  </div>
640
726
  </div>
641
727
  `
642
728
  : nothing}
643
729
 
644
- <!-- Error overlay -->
645
- ${!s.shouldShowIdleScreen && s.error
730
+ <!-- Fatal error overlay (blocking) — auto-dismisses on playback resume -->
731
+ ${!s.shouldShowIdleScreen && this._displayedError && !this._displayedIsPassive
646
732
  ? html`
647
733
  <div
648
734
  role="alert"
649
735
  aria-live="assertive"
650
- class=${classMap({
651
- "fw-error-overlay": true,
652
- "fw-error-overlay--passive": s.isPassiveError,
653
- "fw-error-overlay--fullscreen": !s.isPassiveError,
654
- })}
736
+ class="fw-error-overlay fw-error-overlay--fullscreen"
737
+ style="transition:opacity 300ms;opacity:${this._isErrorDismissing ? "0" : "1"}"
655
738
  >
656
- <div
657
- class=${classMap({
658
- "fw-error-popup": true,
659
- "fw-error-popup--passive": s.isPassiveError,
660
- "fw-error-popup--fullscreen": !s.isPassiveError,
661
- })}
662
- >
663
- <div
664
- class=${classMap({
665
- "fw-error-header": true,
666
- "fw-error-header--warning": s.isPassiveError,
667
- "fw-error-header--error": !s.isPassiveError,
668
- })}
669
- >
670
- <span
671
- class=${classMap({
672
- "fw-error-title": true,
673
- "fw-error-title--warning": s.isPassiveError,
674
- "fw-error-title--error": !s.isPassiveError,
675
- })}
676
- >${s.isPassiveError ? "Warning" : "Error"}</span
739
+ <div class="fw-error-popup fw-error-popup--fullscreen">
740
+ <div class="fw-error-header fw-error-header--error">
741
+ <span class="fw-error-title fw-error-title--error"
742
+ >${this.pc.t("error")}</span
677
743
  >
678
744
  <button
679
745
  type="button"
680
746
  class="fw-error-close"
681
747
  @click=${() => this.pc.clearError()}
682
- aria-label="Dismiss"
748
+ aria-label=${this.pc.t("dismiss")}
683
749
  >
684
750
  ${closeIcon()}
685
751
  </button>
686
752
  </div>
687
753
  <div class="fw-error-body">
688
- <p class="fw-error-message">Playback issue</p>
754
+ <p class="fw-error-message">${this._displayedError}</p>
689
755
  </div>
690
756
  <div class="fw-error-actions">
691
757
  <button
692
758
  type="button"
693
759
  class="fw-error-btn"
694
- aria-label="Retry playback"
760
+ aria-label=${this.pc.t("retry")}
695
761
  @click=${() => {
696
762
  this.pc.clearError();
697
763
  this.pc.retry();
698
764
  }}
699
765
  >
700
- Retry
766
+ ${this.pc.t("retry")}
767
+ </button>
768
+ ${this.pc.canAttemptFallback()
769
+ ? html`
770
+ <button
771
+ type="button"
772
+ class="fw-error-btn fw-error-btn--secondary"
773
+ aria-label=${this.pc.t("tryNext")}
774
+ @click=${() => {
775
+ this.pc.clearError();
776
+ this.pc.tryNextSource();
777
+ }}
778
+ >
779
+ ${this.pc.t("tryNext")}
780
+ </button>
781
+ `
782
+ : nothing}
783
+ <button
784
+ type="button"
785
+ class="fw-error-btn fw-error-btn--secondary"
786
+ aria-label=${this.pc.t("reloadPlayer")}
787
+ @click=${() => {
788
+ this.pc.clearError();
789
+ this.pc.reload();
790
+ }}
791
+ >
792
+ ${this.pc.t("reloadPlayer")}
701
793
  </button>
702
794
  </div>
703
795
  </div>
@@ -721,7 +813,7 @@ export class FwPlayer extends LitElement {
721
813
  type="button"
722
814
  @click=${() => this.pc.dismissToast()}
723
815
  class="ml-0.5 text-white/60 hover\\:text-white cursor-pointer"
724
- aria-label="Dismiss"
816
+ aria-label=${this.pc.t("dismiss")}
725
817
  >
726
818
  ${closeIcon()}
727
819
  </button>
@@ -730,23 +822,29 @@ export class FwPlayer extends LitElement {
730
822
  `
731
823
  : nothing}
732
824
 
733
- <!-- Player controls -->
825
+ <!-- Player controls: slot allows custom controls, fallback renders defaults -->
734
826
  ${!this._useStockControls
735
827
  ? html`
736
- <fw-player-controls
737
- part="controls"
738
- .pc=${this.pc}
739
- .playbackMode=${this.playbackMode}
740
- .isContentLive=${s.isEffectivelyLive}
741
- .devMode=${this.devMode}
742
- .isStatsOpen=${this._isStatsOpen}
743
- @fw-stats-toggle=${() => {
744
- this._isStatsOpen = !this._isStatsOpen;
745
- }}
746
- @fw-mode-change=${(event: CustomEvent<{ mode: PlaybackMode }>) => {
747
- this.playbackMode = event.detail.mode;
748
- }}
749
- ></fw-player-controls>
828
+ <slot name="controls">
829
+ <fw-player-controls
830
+ part="controls"
831
+ .pc=${this.pc}
832
+ .playbackMode=${this.playbackMode}
833
+ .isContentLive=${s.isEffectivelyLive}
834
+ .devMode=${this.devMode}
835
+ .isStatsOpen=${this._isStatsOpen}
836
+ .activeLocale=${this.locale ?? "en"}
837
+ @fw-stats-toggle=${() => {
838
+ this._isStatsOpen = !this._isStatsOpen;
839
+ }}
840
+ @fw-mode-change=${(event: CustomEvent<{ mode: PlaybackMode }>) => {
841
+ this.playbackMode = event.detail.mode;
842
+ }}
843
+ @fw-locale-change=${(e: CustomEvent) => {
844
+ this.locale = e.detail.locale;
845
+ }}
846
+ ></fw-player-controls>
847
+ </slot>
750
848
  `
751
849
  : nothing}
752
850
  </div>
@@ -776,7 +874,7 @@ export class FwPlayer extends LitElement {
776
874
  data-context-menu="true"
777
875
  data-state=${this._contextMenuState}
778
876
  data-side=${this._contextMenuSide}
779
- class="fw-player-surface fw-context-menu"
877
+ class="fw-context-menu"
780
878
  role="menu"
781
879
  aria-label="Player options"
782
880
  tabindex="-1"
@@ -798,7 +896,7 @@ export class FwPlayer extends LitElement {
798
896
  }}
799
897
  >
800
898
  <span class="opacity-70 shrink-0">${statsIcon(14)}</span>
801
- <span>${this._isStatsOpen ? "Hide Stats" : "Stats"}</span>
899
+ <span>${this._isStatsOpen ? this.pc.t("hideStats") : this.pc.t("showStats")}</span>
802
900
  </button>
803
901
  ${this.devMode
804
902
  ? html`
@@ -816,7 +914,11 @@ export class FwPlayer extends LitElement {
816
914
  }}
817
915
  >
818
916
  <span class="opacity-70 shrink-0">${settingsIcon(14)}</span>
819
- <span>${this._isDevPanelOpen ? "Hide Settings" : "Settings"}</span>
917
+ <span
918
+ >${this._isDevPanelOpen
919
+ ? this.pc.t("hideSettings")
920
+ : this.pc.t("settings")}</span
921
+ >
820
922
  </button>
821
923
  `
822
924
  : nothing}
@@ -835,7 +937,7 @@ export class FwPlayer extends LitElement {
835
937
  }}
836
938
  >
837
939
  <span class="opacity-70 shrink-0">${pictureInPictureIcon(14)}</span>
838
- <span>Picture-in-Picture</span>
940
+ <span>${this.pc.t("pictureInPicture")}</span>
839
941
  </button>
840
942
  <button
841
943
  type="button"
@@ -851,7 +953,7 @@ export class FwPlayer extends LitElement {
851
953
  }}
852
954
  >
853
955
  <span class="opacity-70 shrink-0">${loopIcon(14)}</span>
854
- <span>${s.isLoopEnabled ? "Disable Loop" : "Enable Loop"}</span>
956
+ <span>${s.isLoopEnabled ? this.pc.t("disableLoop") : this.pc.t("enableLoop")}</span>
855
957
  </button>
856
958
  </div>
857
959
  `