@livepeer-frameworks/player-wc 0.1.8 → 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 (141) 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 +41 -26
  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 +104 -39
  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 +722 -499
  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 +3 -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/components/fw-volume-control.d.ts +11 -0
  60. package/dist/types/controllers/player-controller-host.d.ts +7 -1
  61. package/dist/types/index.d.ts +1 -0
  62. package/package.json +10 -13
  63. package/src/components/controls/fw-fullscreen-button.ts +75 -0
  64. package/src/components/controls/fw-live-badge.ts +109 -0
  65. package/src/components/controls/fw-play-button.ts +75 -0
  66. package/src/components/controls/fw-skip-button.ts +59 -0
  67. package/src/components/controls/fw-time-display.ts +74 -0
  68. package/src/components/controls/fw-volume-control.ts +75 -0
  69. package/src/components/controls/index.ts +6 -0
  70. package/src/components/fw-dev-mode-panel.ts +10 -17
  71. package/src/components/fw-error-overlay.ts +13 -5
  72. package/src/components/fw-idle-screen.ts +10 -2
  73. package/src/components/fw-loading-screen.ts +90 -46
  74. package/src/components/fw-loading-spinner.ts +18 -9
  75. package/src/components/fw-player-controls.ts +39 -28
  76. package/src/components/fw-player.ts +166 -64
  77. package/src/components/fw-settings-menu.ts +49 -9
  78. package/src/components/fw-stream-state-overlay.ts +13 -5
  79. package/src/components/fw-toast.ts +11 -1
  80. package/src/components/fw-volume-control.ts +112 -43
  81. package/src/controllers/player-controller-host.ts +18 -0
  82. package/src/index.ts +10 -0
  83. package/src/styles/shared-styles.ts +401 -304
  84. package/dist/cjs/components/fw-context-menu.js +0 -17
  85. package/dist/cjs/components/fw-context-menu.js.map +0 -1
  86. package/dist/cjs/components/fw-dev-mode-panel.js +0 -907
  87. package/dist/cjs/components/fw-dev-mode-panel.js.map +0 -1
  88. package/dist/cjs/components/fw-dvd-logo.js +0 -211
  89. package/dist/cjs/components/fw-dvd-logo.js.map +0 -1
  90. package/dist/cjs/components/fw-error-overlay.js +0 -101
  91. package/dist/cjs/components/fw-error-overlay.js.map +0 -1
  92. package/dist/cjs/components/fw-idle-screen.js +0 -726
  93. package/dist/cjs/components/fw-idle-screen.js.map +0 -1
  94. package/dist/cjs/components/fw-loading-screen.js +0 -513
  95. package/dist/cjs/components/fw-loading-screen.js.map +0 -1
  96. package/dist/cjs/components/fw-loading-spinner.js +0 -62
  97. package/dist/cjs/components/fw-loading-spinner.js.map +0 -1
  98. package/dist/cjs/components/fw-player-controls.js +0 -441
  99. package/dist/cjs/components/fw-player-controls.js.map +0 -1
  100. package/dist/cjs/components/fw-player.js +0 -832
  101. package/dist/cjs/components/fw-player.js.map +0 -1
  102. package/dist/cjs/components/fw-seek-bar.js +0 -383
  103. package/dist/cjs/components/fw-seek-bar.js.map +0 -1
  104. package/dist/cjs/components/fw-settings-menu.js +0 -253
  105. package/dist/cjs/components/fw-settings-menu.js.map +0 -1
  106. package/dist/cjs/components/fw-skip-indicator.js +0 -143
  107. package/dist/cjs/components/fw-skip-indicator.js.map +0 -1
  108. package/dist/cjs/components/fw-speed-indicator.js +0 -61
  109. package/dist/cjs/components/fw-speed-indicator.js.map +0 -1
  110. package/dist/cjs/components/fw-stats-panel.js +0 -205
  111. package/dist/cjs/components/fw-stats-panel.js.map +0 -1
  112. package/dist/cjs/components/fw-stream-state-overlay.js +0 -338
  113. package/dist/cjs/components/fw-stream-state-overlay.js.map +0 -1
  114. package/dist/cjs/components/fw-subtitle-renderer.js +0 -217
  115. package/dist/cjs/components/fw-subtitle-renderer.js.map +0 -1
  116. package/dist/cjs/components/fw-thumbnail-overlay.js +0 -161
  117. package/dist/cjs/components/fw-thumbnail-overlay.js.map +0 -1
  118. package/dist/cjs/components/fw-title-overlay.js +0 -72
  119. package/dist/cjs/components/fw-title-overlay.js.map +0 -1
  120. package/dist/cjs/components/fw-toast.js +0 -74
  121. package/dist/cjs/components/fw-toast.js.map +0 -1
  122. package/dist/cjs/components/fw-volume-control.js +0 -221
  123. package/dist/cjs/components/fw-volume-control.js.map +0 -1
  124. package/dist/cjs/components/shared/hitmarker-audio.js +0 -76
  125. package/dist/cjs/components/shared/hitmarker-audio.js.map +0 -1
  126. package/dist/cjs/constants/media-assets.js +0 -11
  127. package/dist/cjs/constants/media-assets.js.map +0 -1
  128. package/dist/cjs/controllers/player-controller-host.js +0 -364
  129. package/dist/cjs/controllers/player-controller-host.js.map +0 -1
  130. package/dist/cjs/define.js +0 -53
  131. package/dist/cjs/define.js.map +0 -1
  132. package/dist/cjs/icons/index.js +0 -180
  133. package/dist/cjs/icons/index.js.map +0 -1
  134. package/dist/cjs/index.js +0 -108
  135. package/dist/cjs/index.js.map +0 -1
  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 +0 -33
  137. 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
  138. package/dist/cjs/styles/shared-styles.js +0 -1985
  139. package/dist/cjs/styles/shared-styles.js.map +0 -1
  140. package/dist/cjs/styles/utility-styles.js +0 -725
  141. package/dist/cjs/styles/utility-styles.js.map +0 -1
@@ -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
  `
@@ -9,8 +9,10 @@ import { utilityStyles } from "../styles/utility-styles.js";
9
9
  import {
10
10
  SPEED_PRESETS,
11
11
  supportsPlaybackRate as coreSupportsPlaybackRate,
12
+ getAvailableLocales,
13
+ getLocaleDisplayName,
12
14
  } from "@livepeer-frameworks/player-core";
13
- import type { PlaybackMode } from "@livepeer-frameworks/player-core";
15
+ import type { PlaybackMode, FwLocale } from "@livepeer-frameworks/player-core";
14
16
  import type { PlayerControllerHost } from "../controllers/player-controller-host.js";
15
17
 
16
18
  @customElement("fw-settings-menu")
@@ -23,6 +25,7 @@ export class FwSettingsMenu extends LitElement {
23
25
  @property({ type: String, attribute: "quality-value" }) qualityValue?: string;
24
26
  @property({ type: String, attribute: "caption-value" }) captionValue?: string;
25
27
  @property({ type: Boolean, attribute: "supports-playback-rate" }) supportsPlaybackRate?: boolean;
28
+ @property({ attribute: "active-locale" }) activeLocale?: FwLocale;
26
29
 
27
30
  @state() private _playbackRate = 1;
28
31
 
@@ -109,6 +112,16 @@ export class FwSettingsMenu extends LitElement {
109
112
  this._close();
110
113
  }
111
114
 
115
+ private _handleLocaleChange(locale: FwLocale): void {
116
+ this.dispatchEvent(
117
+ new CustomEvent("fw-locale-change", {
118
+ detail: { locale },
119
+ bubbles: true,
120
+ composed: true,
121
+ })
122
+ );
123
+ }
124
+
112
125
  private _deriveFallbackQualities(): Array<{
113
126
  id: string;
114
127
  label: string;
@@ -166,11 +179,11 @@ export class FwSettingsMenu extends LitElement {
166
179
  this.supportsPlaybackRate ?? coreSupportsPlaybackRate(state.videoElement);
167
180
 
168
181
  return html`
169
- <div class="fw-player-surface fw-settings-menu" role="menu" aria-label="Player settings">
182
+ <div class="fw-settings-menu" role="menu" aria-label=${this.pc.t("settings")}>
170
183
  ${this.isContentLive
171
184
  ? html`
172
185
  <div class="fw-settings-section">
173
- <div class="fw-settings-label">Mode</div>
186
+ <div class="fw-settings-label">${this.pc.t("mode")}</div>
174
187
  <div class="fw-settings-options">
175
188
  ${(["auto", "low-latency", "quality"] as const).map(
176
189
  (mode) => html`
@@ -182,7 +195,11 @@ export class FwSettingsMenu extends LitElement {
182
195
  })}
183
196
  @click=${() => this._handleModeChange(mode)}
184
197
  >
185
- ${mode === "low-latency" ? "Fast" : mode === "quality" ? "Stable" : "Auto"}
198
+ ${mode === "low-latency"
199
+ ? this.pc.t("fast")
200
+ : mode === "quality"
201
+ ? this.pc.t("stable")
202
+ : this.pc.t("auto")}
186
203
  </button>
187
204
  `
188
205
  )}
@@ -193,7 +210,7 @@ export class FwSettingsMenu extends LitElement {
193
210
  ${supportsPlaybackRate
194
211
  ? html`
195
212
  <div class="fw-settings-section">
196
- <div class="fw-settings-label">Speed</div>
213
+ <div class="fw-settings-label">${this.pc.t("speed")}</div>
197
214
  <div class="fw-settings-options fw-settings-options--wrap">
198
215
  ${SPEED_PRESETS.map(
199
216
  (rate) => html`
@@ -216,7 +233,7 @@ export class FwSettingsMenu extends LitElement {
216
233
  ${qualities.length > 0
217
234
  ? html`
218
235
  <div class="fw-settings-section">
219
- <div class="fw-settings-label">Quality</div>
236
+ <div class="fw-settings-label">${this.pc.t("quality")}</div>
220
237
  <div class="fw-settings-list">
221
238
  <button
222
239
  type="button"
@@ -226,7 +243,7 @@ export class FwSettingsMenu extends LitElement {
226
243
  })}
227
244
  @click=${() => this._handleQualityChange("auto")}
228
245
  >
229
- Auto
246
+ ${this.pc.t("auto")}
230
247
  </button>
231
248
  ${qualities.map(
232
249
  (quality) => html`
@@ -249,7 +266,7 @@ export class FwSettingsMenu extends LitElement {
249
266
  ${textTracks.length > 0
250
267
  ? html`
251
268
  <div class="fw-settings-section">
252
- <div class="fw-settings-label">Captions</div>
269
+ <div class="fw-settings-label">${this.pc.t("captions")}</div>
253
270
  <div class="fw-settings-list">
254
271
  <button
255
272
  type="button"
@@ -259,7 +276,7 @@ export class FwSettingsMenu extends LitElement {
259
276
  })}
260
277
  @click=${() => this._handleCaptionChange("none")}
261
278
  >
262
- Off
279
+ ${this.pc.t("captionsOff")}
263
280
  </button>
264
281
  ${textTracks.map(
265
282
  (track) => html`
@@ -279,6 +296,29 @@ export class FwSettingsMenu extends LitElement {
279
296
  </div>
280
297
  `
281
298
  : nothing}
299
+ ${this.activeLocale !== undefined
300
+ ? html`
301
+ <div class="fw-settings-section">
302
+ <div class="fw-settings-label">${this.pc.t("language")}</div>
303
+ <div class="fw-settings-list">
304
+ ${getAvailableLocales().map(
305
+ (loc) => html`
306
+ <button
307
+ type="button"
308
+ class=${classMap({
309
+ "fw-settings-list-item": true,
310
+ "fw-settings-list-item--active": this.activeLocale === loc,
311
+ })}
312
+ @click=${() => this._handleLocaleChange(loc)}
313
+ >
314
+ ${getLocaleDisplayName(loc)}
315
+ </button>
316
+ `
317
+ )}
318
+ </div>
319
+ </div>
320
+ `
321
+ : nothing}
282
322
  </div>
283
323
  `;
284
324
  }
@@ -1,6 +1,7 @@
1
1
  import { LitElement, css, html, nothing } from "lit";
2
2
  import { customElement, property } from "lit/decorators.js";
3
3
  import type { StreamStatus } from "@livepeer-frameworks/player-core";
4
+ import { createTranslator, type TranslateFn } from "@livepeer-frameworks/player-core";
4
5
  import { sharedStyles } from "../styles/shared-styles.js";
5
6
 
6
7
  @customElement("fw-stream-state-overlay")
@@ -11,6 +12,13 @@ export class FwStreamStateOverlay extends LitElement {
11
12
  @property({ type: Boolean }) visible = true;
12
13
  @property({ type: Boolean, attribute: "retry-enabled" }) retryEnabled = false;
13
14
  @property({ attribute: false }) onRetry?: () => void;
15
+ @property({ attribute: false }) translator?: TranslateFn;
16
+
17
+ private _defaultTranslator: TranslateFn = createTranslator({ locale: "en" });
18
+
19
+ private get _t(): TranslateFn {
20
+ return this.translator ?? this._defaultTranslator;
21
+ }
14
22
 
15
23
  static styles = [
16
24
  sharedStyles,
@@ -294,15 +302,15 @@ export class FwStreamStateOverlay extends LitElement {
294
302
  `
295
303
  : nothing}
296
304
  ${this.status === "OFFLINE"
297
- ? html`<p class="hint">The stream will start when the broadcaster goes live</p>`
305
+ ? html`<p class="hint">${this._t("broadcasterGoLive")}</p>`
298
306
  : nothing}
299
307
  ${this.status === "BOOTING" || this.status === "WAITING_FOR_DATA"
300
- ? html`<p class="hint">Please wait while the stream prepares...</p>`
308
+ ? html`<p class="hint">${this._t("streamPreparing")}</p>`
301
309
  : nothing}
302
310
  ${!showRetry
303
311
  ? html`<div class="polling-indicator">
304
312
  <span class="polling-dot"></span>
305
- <span>Checking stream status...</span>
313
+ <span>${this._t("checkingStatus")}</span>
306
314
  </div>`
307
315
  : nothing}
308
316
  </div>
@@ -312,9 +320,9 @@ export class FwStreamStateOverlay extends LitElement {
312
320
  type="button"
313
321
  class="btn-flush"
314
322
  @click=${this._handleRetry}
315
- aria-label="Retry connection"
323
+ aria-label=${this._t("retryConnection")}
316
324
  >
317
- Retry Connection
325
+ ${this._t("retryConnection")}
318
326
  </button>
319
327
  </div>`
320
328
  : nothing}
@@ -1,10 +1,18 @@
1
1
  import { LitElement, html, css, nothing } from "lit";
2
2
  import { customElement, property } from "lit/decorators.js";
3
3
  import { closeIcon } from "../icons/index.js";
4
+ import { createTranslator, type TranslateFn } from "@livepeer-frameworks/player-core";
4
5
 
5
6
  @customElement("fw-toast")
6
7
  export class FwToast extends LitElement {
7
8
  @property({ type: String }) message = "";
9
+ @property({ attribute: false }) translator?: TranslateFn;
10
+
11
+ private _defaultTranslator: TranslateFn = createTranslator({ locale: "en" });
12
+
13
+ private get _t(): TranslateFn {
14
+ return this.translator ?? this._defaultTranslator;
15
+ }
8
16
 
9
17
  static styles = css`
10
18
  :host {
@@ -53,7 +61,9 @@ export class FwToast extends LitElement {
53
61
  return html`
54
62
  <div class="toast">
55
63
  <span>${this.message}</span>
56
- <button type="button" @click=${this._dismiss} aria-label="Dismiss">${closeIcon()}</button>
64
+ <button type="button" @click=${this._dismiss} aria-label=${this._t("dismiss")}>
65
+ ${closeIcon()}
66
+ </button>
57
67
  </div>
58
68
  `;
59
69
  }