@livepeer-frameworks/player-wc 0.1.3 → 0.1.5

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 (35) hide show
  1. package/dist/cjs/components/fw-dev-mode-panel.js +1 -0
  2. package/dist/cjs/components/fw-dev-mode-panel.js.map +1 -1
  3. package/dist/cjs/components/fw-player-controls.js +12 -3
  4. package/dist/cjs/components/fw-player-controls.js.map +1 -1
  5. package/dist/cjs/components/fw-player.js +50 -10
  6. package/dist/cjs/components/fw-player.js.map +1 -1
  7. package/dist/cjs/components/fw-settings-menu.js +18 -1
  8. package/dist/cjs/components/fw-settings-menu.js.map +1 -1
  9. package/dist/cjs/controllers/player-controller-host.js +23 -1
  10. package/dist/cjs/controllers/player-controller-host.js.map +1 -1
  11. package/dist/cjs/styles/shared-styles.js +18 -0
  12. package/dist/cjs/styles/shared-styles.js.map +1 -1
  13. package/dist/esm/components/fw-dev-mode-panel.js +1 -0
  14. package/dist/esm/components/fw-dev-mode-panel.js.map +1 -1
  15. package/dist/esm/components/fw-player-controls.js +12 -3
  16. package/dist/esm/components/fw-player-controls.js.map +1 -1
  17. package/dist/esm/components/fw-player.js +50 -10
  18. package/dist/esm/components/fw-player.js.map +1 -1
  19. package/dist/esm/components/fw-settings-menu.js +18 -1
  20. package/dist/esm/components/fw-settings-menu.js.map +1 -1
  21. package/dist/esm/controllers/player-controller-host.js +23 -1
  22. package/dist/esm/controllers/player-controller-host.js.map +1 -1
  23. package/dist/esm/styles/shared-styles.js +18 -0
  24. package/dist/esm/styles/shared-styles.js.map +1 -1
  25. package/dist/fw-player.iife.js +40 -20
  26. package/dist/types/components/fw-player-controls.d.ts +1 -0
  27. package/dist/types/components/fw-player.d.ts +4 -0
  28. package/dist/types/components/fw-settings-menu.d.ts +1 -0
  29. package/package.json +2 -2
  30. package/src/components/fw-dev-mode-panel.ts +1 -0
  31. package/src/components/fw-player-controls.ts +10 -3
  32. package/src/components/fw-player.ts +50 -10
  33. package/src/components/fw-settings-menu.ts +41 -1
  34. package/src/controllers/player-controller-host.ts +23 -1
  35. package/src/styles/shared-styles.ts +18 -0
@@ -9,6 +9,7 @@ export declare class FwPlayerControls extends LitElement {
9
9
  pc: PlayerControllerHost;
10
10
  playbackMode: PlaybackMode;
11
11
  isContentLive: boolean;
12
+ devMode: boolean;
12
13
  showStatsButton: boolean;
13
14
  isStatsOpen: boolean;
14
15
  private _settingsOpen;
@@ -14,6 +14,8 @@ export declare class FwPlayer extends LitElement {
14
14
  autoplay: boolean;
15
15
  muted: boolean;
16
16
  controls: boolean;
17
+ stockControls: boolean;
18
+ nativeControls: boolean;
17
19
  debug: boolean;
18
20
  devMode: boolean;
19
21
  thumbnailUrl?: string;
@@ -45,6 +47,8 @@ export declare class FwPlayer extends LitElement {
45
47
  private _closeContextMenu;
46
48
  private _getQueryRoot;
47
49
  private _getContextMenuElement;
50
+ private _getContextMenuBounds;
51
+ private _toLocalContextMenuPoint;
48
52
  private _getContextMenuItems;
49
53
  private _focusFirstContextMenuItem;
50
54
  private _focusPlaybackModeTrigger;
@@ -21,6 +21,7 @@ export declare class FwSettingsMenu extends LitElement {
21
21
  private _handleSpeedChange;
22
22
  private _handleQualityChange;
23
23
  private _handleCaptionChange;
24
+ private _deriveFallbackQualities;
24
25
  protected render(): import("lit").TemplateResult<1> | typeof nothing;
25
26
  }
26
27
  declare global {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livepeer-frameworks/player-wc",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "Lit Web Components for FrameWorks streaming player — <fw-player> custom element with full UI",
6
6
  "main": "dist/cjs/index.js",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "lit": "^3.2.0",
32
- "@livepeer-frameworks/player-core": "0.1.3"
32
+ "@livepeer-frameworks/player-core": "0.1.4"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@rollup/plugin-commonjs": "^29.0.0",
@@ -69,6 +69,7 @@ export class FwDevModePanel extends LitElement {
69
69
  :host {
70
70
  display: block;
71
71
  height: 100%;
72
+ min-height: 0;
72
73
  }
73
74
  `,
74
75
  ];
@@ -48,6 +48,7 @@ export class FwPlayerControls extends LitElement {
48
48
  @property({ attribute: false }) pc!: PlayerControllerHost;
49
49
  @property({ type: String }) playbackMode: PlaybackMode = "auto";
50
50
  @property({ type: Boolean, attribute: "is-content-live" }) isContentLive = false;
51
+ @property({ type: Boolean, attribute: "dev-mode" }) devMode = false;
51
52
  @property({ type: Boolean, attribute: "show-stats-button" }) showStatsButton = false;
52
53
  @property({ type: Boolean, attribute: "is-stats-open" }) isStatsOpen = false;
53
54
 
@@ -314,6 +315,13 @@ export class FwPlayerControls extends LitElement {
314
315
  const state = this.pc.s;
315
316
  const disabled = !state.videoElement;
316
317
  const context = this._getSeekingContext();
318
+ const shouldShowControls =
319
+ state.shouldShowControls ||
320
+ state.isPaused ||
321
+ !state.hasPlaybackStarted ||
322
+ state.shouldShowIdleScreen ||
323
+ !!state.error ||
324
+ this._settingsOpen;
317
325
 
318
326
  const timeDisplay = formatTimeDisplay({
319
327
  isLive: context.isLive,
@@ -331,8 +339,8 @@ export class FwPlayerControls extends LitElement {
331
339
  class=${classMap({
332
340
  "fw-player-surface": true,
333
341
  "fw-controls-wrapper": true,
334
- "fw-controls-wrapper--visible": state.shouldShowControls,
335
- "fw-controls-wrapper--hidden": !state.shouldShowControls,
342
+ "fw-controls-wrapper--visible": shouldShowControls,
343
+ "fw-controls-wrapper--hidden": !shouldShowControls,
336
344
  })}
337
345
  >
338
346
  <div class="fw-control-bar" @click=${(event: Event) => event.stopPropagation()}>
@@ -453,7 +461,6 @@ export class FwPlayerControls extends LitElement {
453
461
  </div>
454
462
  `
455
463
  : nothing}
456
-
457
464
  <div class="fw-control-group fw-settings-anchor">
458
465
  <button
459
466
  type="button"
@@ -27,7 +27,11 @@ export class FwPlayer extends LitElement {
27
27
  @property({ attribute: "auth-token" }) authToken?: string;
28
28
  @property({ type: Boolean }) autoplay = true;
29
29
  @property({ type: Boolean }) muted = true;
30
+ // React/Svelte use `stockControls` for native controls. Keep `controls` as a
31
+ // compatibility no-op so WC parity does not hide custom controls/seekbar.
30
32
  @property({ type: Boolean }) controls = false;
33
+ @property({ type: Boolean, attribute: "stock-controls" }) stockControls = false;
34
+ @property({ type: Boolean, attribute: "native-controls" }) nativeControls = false;
31
35
  @property({ type: Boolean }) debug = false;
32
36
  @property({ type: Boolean, attribute: "dev-mode" }) devMode = false;
33
37
  @property({ attribute: "thumbnail-url" }) thumbnailUrl?: string;
@@ -78,6 +82,7 @@ export class FwPlayer extends LitElement {
78
82
  .player-area--dev {
79
83
  flex: 1;
80
84
  min-width: 0;
85
+ min-height: 0;
81
86
  }
82
87
  `,
83
88
  ];
@@ -93,7 +98,8 @@ export class FwPlayer extends LitElement {
93
98
  changed.has("authToken") ||
94
99
  changed.has("autoplay") ||
95
100
  changed.has("muted") ||
96
- changed.has("controls") ||
101
+ changed.has("stockControls") ||
102
+ changed.has("nativeControls") ||
97
103
  changed.has("debug") ||
98
104
  changed.has("thumbnailUrl") ||
99
105
  changed.has("endpoints")
@@ -107,7 +113,7 @@ export class FwPlayer extends LitElement {
107
113
  authToken: this.authToken,
108
114
  autoplay: this.autoplay,
109
115
  muted: this.muted,
110
- controls: this.controls,
116
+ controls: this.stockControls || this.nativeControls,
111
117
  poster: this.thumbnailUrl,
112
118
  debug: this.debug,
113
119
  });
@@ -199,6 +205,31 @@ export class FwPlayer extends LitElement {
199
205
  private _getContextMenuElement = () =>
200
206
  this._getQueryRoot()?.querySelector<HTMLElement>('[data-context-menu="true"]') ?? null;
201
207
 
208
+ private _getContextMenuBounds = () => {
209
+ const root = this._getQueryRoot()?.querySelector<HTMLElement>('[part="root"]');
210
+ const rect = root?.getBoundingClientRect() ?? this.getBoundingClientRect();
211
+
212
+ const width = rect.width > 0 ? rect.width : window.innerWidth;
213
+ const height = rect.height > 0 ? rect.height : window.innerHeight;
214
+
215
+ return {
216
+ left: rect.left,
217
+ top: rect.top,
218
+ right: rect.left + width,
219
+ bottom: rect.top + height,
220
+ width,
221
+ height,
222
+ };
223
+ };
224
+
225
+ private _toLocalContextMenuPoint = (clientX: number, clientY: number) => {
226
+ const bounds = this._getContextMenuBounds();
227
+ return {
228
+ x: clientX - bounds.left,
229
+ y: clientY - bounds.top,
230
+ };
231
+ };
232
+
202
233
  private _getContextMenuItems = (level: "root" | "submenu" = this._contextMenuActiveLevel) =>
203
234
  Array.from(
204
235
  this._getQueryRoot()?.querySelectorAll<HTMLButtonElement>(
@@ -241,8 +272,9 @@ export class FwPlayer extends LitElement {
241
272
 
242
273
  private _clampContextMenuPosition = (x: number, y: number, width: number, height: number) => {
243
274
  const viewportPadding = 8;
244
- const maxX = Math.max(viewportPadding, window.innerWidth - width - viewportPadding);
245
- const maxY = Math.max(viewportPadding, window.innerHeight - height - viewportPadding);
275
+ const bounds = this._getContextMenuBounds();
276
+ const maxX = Math.max(viewportPadding, bounds.width - width - viewportPadding);
277
+ const maxY = Math.max(viewportPadding, bounds.height - height - viewportPadding);
246
278
 
247
279
  return {
248
280
  x: Math.max(viewportPadding, Math.min(x, maxX)),
@@ -278,15 +310,17 @@ export class FwPlayer extends LitElement {
278
310
  if (this._contextMenuOpenSubmenu !== "playback-mode") return;
279
311
  const menu = this._getContextMenuElement();
280
312
  if (!menu) return;
313
+ const bounds = this._getContextMenuBounds();
281
314
  const rect = menu.getBoundingClientRect();
282
315
  const estimatedSubmenuWidth = 190;
283
316
  this._contextMenuSubmenuSide =
284
- rect.right + estimatedSubmenuWidth > window.innerWidth - 8 ? "left" : "right";
317
+ rect.right + estimatedSubmenuWidth > bounds.right - 8 ? "left" : "right";
285
318
  };
286
319
 
287
- private _openContextMenu = (x: number, y: number) => {
288
- const next = this._clampContextMenuPosition(x, y, 220, 200);
289
- this._contextMenuSide = this._resolveContextMenuSide(x, y, next.x, next.y);
320
+ private _openContextMenu = (clientX: number, clientY: number) => {
321
+ const local = this._toLocalContextMenuPoint(clientX, clientY);
322
+ const next = this._clampContextMenuPosition(local.x, local.y, 220, 200);
323
+ this._contextMenuSide = this._resolveContextMenuSide(local.x, local.y, next.x, next.y);
290
324
  this._contextMenuX = next.x;
291
325
  this._contextMenuY = next.y;
292
326
  this._contextMenuMounted = true;
@@ -488,7 +522,11 @@ export class FwPlayer extends LitElement {
488
522
  }
489
523
 
490
524
  private get _useStockControls() {
491
- return this.controls || this.pc.s.currentPlayerInfo?.shortname === "mist-legacy";
525
+ return (
526
+ this.stockControls ||
527
+ this.nativeControls ||
528
+ this.pc.s.currentPlayerInfo?.shortname === "mist-legacy"
529
+ );
492
530
  }
493
531
 
494
532
  // ---- Public API methods ----
@@ -773,6 +811,7 @@ export class FwPlayer extends LitElement {
773
811
  .pc=${this.pc}
774
812
  .playbackMode=${this.playbackMode}
775
813
  .isContentLive=${s.isEffectivelyLive}
814
+ .devMode=${this.devMode}
776
815
  .isStatsOpen=${this._isStatsOpen}
777
816
  @fw-stats-toggle=${() => {
778
817
  this._isStatsOpen = !this._isStatsOpen;
@@ -814,7 +853,8 @@ export class FwPlayer extends LitElement {
814
853
  role="menu"
815
854
  aria-label="Player options"
816
855
  tabindex="-1"
817
- style="position: fixed; left: ${this._contextMenuX}px; top: ${this._contextMenuY}px;"
856
+ style="position: absolute; left: ${this._contextMenuX}px; top: ${this
857
+ ._contextMenuY}px;"
818
858
  @contextmenu=${(e: MouseEvent) => e.preventDefault()}
819
859
  @keydown=${this._handleContextMenuKeyDown}
820
860
  >
@@ -109,13 +109,53 @@ export class FwSettingsMenu extends LitElement {
109
109
  this._close();
110
110
  }
111
111
 
112
+ private _deriveFallbackQualities(): Array<{
113
+ id: string;
114
+ label: string;
115
+ bitrate?: number;
116
+ width?: number;
117
+ height?: number;
118
+ isAuto?: boolean;
119
+ active?: boolean;
120
+ }> {
121
+ const tracks = (
122
+ this.pc?.s.streamState?.streamInfo as
123
+ | {
124
+ meta?: {
125
+ tracks?: Record<
126
+ string,
127
+ { type?: string; codec?: string; width?: number; height?: number; bps?: number }
128
+ >;
129
+ };
130
+ }
131
+ | undefined
132
+ )?.meta?.tracks;
133
+
134
+ if (!tracks) {
135
+ return [];
136
+ }
137
+
138
+ return Object.entries(tracks)
139
+ .filter(([, track]) => track?.type === "video")
140
+ .map(([id, track]) => ({
141
+ id,
142
+ label: track.height ? `${track.height}p` : (track.codec ?? id),
143
+ width: track.width,
144
+ height: track.height,
145
+ bitrate: track.bps,
146
+ }))
147
+ .sort((a, b) => (b.height ?? 0) - (a.height ?? 0));
148
+ }
149
+
112
150
  protected render() {
113
151
  if (!this.open) {
114
152
  return nothing;
115
153
  }
116
154
 
117
155
  const state = this.pc.s;
118
- const qualities = state.qualities ?? [];
156
+ const controllerQualities = state.qualities ?? [];
157
+ const qualities =
158
+ controllerQualities.length > 0 ? controllerQualities : this._deriveFallbackQualities();
119
159
  const textTracks = state.textTracks ?? [];
120
160
  const activeQuality =
121
161
  this.qualityValue ?? qualities.find((quality) => quality.active)?.id ?? "auto";
@@ -216,7 +216,18 @@ export class PlayerControllerHost implements ReactiveController {
216
216
 
217
217
  u.push(
218
218
  controller.on("timeUpdate", ({ currentTime, duration }) => {
219
- this.update({ currentTime, duration });
219
+ const next: Partial<PlayerControllerHostState> = {
220
+ currentTime,
221
+ duration,
222
+ shouldShowControls: controller.shouldShowControls(),
223
+ };
224
+ if (this.s.qualities.length === 0) {
225
+ const qualities = controller.getQualities();
226
+ if (qualities.length > 0) {
227
+ next.qualities = qualities;
228
+ }
229
+ }
230
+ this.update(next);
220
231
  this.dispatchEvent("fw-time-update", { currentTime, duration });
221
232
  })
222
233
  );
@@ -252,6 +263,7 @@ export class PlayerControllerHost implements ReactiveController {
252
263
  textTracks: controller.getTextTracks(),
253
264
  });
254
265
  this.dispatchEvent("fw-ready", { videoElement });
266
+ this.syncState();
255
267
 
256
268
  const handleVideoEvent = () => {
257
269
  if (this.controller?.shouldSuppressVideoEvents?.()) return;
@@ -278,6 +290,7 @@ export class PlayerControllerHost implements ReactiveController {
278
290
  qualities: controller.getQualities(),
279
291
  textTracks: controller.getTextTracks(),
280
292
  });
293
+ this.syncState();
281
294
  })
282
295
  );
283
296
 
@@ -458,15 +471,24 @@ export class PlayerControllerHost implements ReactiveController {
458
471
 
459
472
  handleMouseEnter() {
460
473
  this.controller?.handleMouseEnter();
474
+ this.update({ isHovering: true, shouldShowControls: true });
461
475
  }
462
476
  handleMouseLeave() {
463
477
  this.controller?.handleMouseLeave();
478
+ this.update({
479
+ isHovering: false,
480
+ shouldShowControls: this.controller?.shouldShowControls() ?? false,
481
+ });
464
482
  }
465
483
  handleMouseMove() {
466
484
  this.controller?.handleMouseMove();
485
+ if (this.controller) {
486
+ this.update({ shouldShowControls: this.controller.shouldShowControls() });
487
+ }
467
488
  }
468
489
  handleTouchStart() {
469
490
  this.controller?.handleTouchStart();
491
+ this.update({ shouldShowControls: true });
470
492
  }
471
493
 
472
494
  async setDevModeOptions(options: {
@@ -442,11 +442,20 @@ export const sharedStyles = css`
442
442
  .fw-context-menu-item {
443
443
  position: relative;
444
444
  display: flex;
445
+ width: 100%;
446
+ justify-content: flex-start;
447
+ gap: 0.5rem;
445
448
  cursor: pointer;
446
449
  user-select: none;
447
450
  align-items: center;
448
451
  padding: 0.5rem 0.75rem;
449
452
  font-size: 0.875rem;
453
+ text-align: left;
454
+ font: inherit;
455
+ border: none;
456
+ background: transparent;
457
+ appearance: none;
458
+ -webkit-appearance: none;
450
459
  outline: none;
451
460
  color: hsl(var(--tn-fg));
452
461
  transition:
@@ -485,11 +494,20 @@ export const sharedStyles = css`
485
494
  .fw-context-menu-checkbox {
486
495
  position: relative;
487
496
  display: flex;
497
+ width: 100%;
498
+ justify-content: flex-start;
499
+ gap: 0.5rem;
488
500
  cursor: pointer;
489
501
  user-select: none;
490
502
  align-items: center;
491
503
  padding: 0.5rem 0.5rem 0.5rem 2rem;
492
504
  font-size: 0.875rem;
505
+ text-align: left;
506
+ font: inherit;
507
+ border: none;
508
+ background: transparent;
509
+ appearance: none;
510
+ -webkit-appearance: none;
493
511
  outline: none;
494
512
  color: hsl(var(--tn-fg));
495
513
  transition: