@livepeer-frameworks/player-wc 0.2.11 → 0.3.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 (42) hide show
  1. package/dist/esm/components/controls/fw-fullscreen-button.js +1 -1
  2. package/dist/esm/components/controls/fw-live-badge.js +1 -1
  3. package/dist/esm/components/controls/fw-play-button.js +1 -1
  4. package/dist/esm/components/controls/fw-skip-button.js +1 -1
  5. package/dist/esm/components/controls/fw-time-display.js +1 -1
  6. package/dist/esm/components/controls/fw-volume-control.js +1 -1
  7. package/dist/esm/components/fw-context-menu.js +1 -1
  8. package/dist/esm/components/fw-dev-mode-panel.js +1 -1
  9. package/dist/esm/components/fw-dvd-logo.js +1 -1
  10. package/dist/esm/components/fw-error-overlay.js +1 -1
  11. package/dist/esm/components/fw-idle-screen.js +1 -1
  12. package/dist/esm/components/fw-loading-screen.js +1 -1
  13. package/dist/esm/components/fw-loading-spinner.js +1 -1
  14. package/dist/esm/components/fw-player-controls.js +3 -1
  15. package/dist/esm/components/fw-player-controls.js.map +1 -1
  16. package/dist/esm/components/fw-player.js +1 -1
  17. package/dist/esm/components/fw-seek-bar.js +82 -2
  18. package/dist/esm/components/fw-seek-bar.js.map +1 -1
  19. package/dist/esm/components/fw-settings-menu.js +1 -1
  20. package/dist/esm/components/fw-skip-indicator.js +1 -1
  21. package/dist/esm/components/fw-speed-indicator.js +1 -1
  22. package/dist/esm/components/fw-stats-panel.js +1 -1
  23. package/dist/esm/components/fw-stream-state-overlay.js +1 -1
  24. package/dist/esm/components/fw-subtitle-renderer.js +1 -1
  25. package/dist/esm/components/fw-thumbnail-overlay.js +1 -1
  26. package/dist/esm/components/fw-title-overlay.js +1 -1
  27. package/dist/esm/components/fw-toast.js +1 -1
  28. package/dist/esm/components/fw-volume-control.js +1 -1
  29. package/dist/esm/controllers/player-controller-host.js +4 -0
  30. package/dist/esm/controllers/player-controller-host.js.map +1 -1
  31. package/dist/esm/node_modules/.pnpm/{@rollup_plugin-typescript@12.3.0_rollup@4.59.0_tslib@2.8.1_typescript@5.9.3 → @rollup_plugin-typescript@12.3.0_rollup@4.60.1_tslib@2.8.1_typescript@6.0.2}/node_modules/tslib/tslib.es6.js.map +1 -1
  32. package/dist/esm/styles/shared-styles.js +17 -1
  33. package/dist/esm/styles/shared-styles.js.map +1 -1
  34. package/dist/fw-player.iife.js +104 -82
  35. package/dist/types/components/fw-seek-bar.d.ts +9 -0
  36. package/dist/types/controllers/player-controller-host.d.ts +2 -1
  37. package/package.json +8 -8
  38. package/src/components/fw-player-controls.ts +2 -0
  39. package/src/components/fw-seek-bar.ts +84 -1
  40. package/src/controllers/player-controller-host.ts +9 -0
  41. package/src/styles/shared-styles.ts +17 -1
  42. /package/dist/esm/node_modules/.pnpm/{@rollup_plugin-typescript@12.3.0_rollup@4.59.0_tslib@2.8.1_typescript@5.9.3 → @rollup_plugin-typescript@12.3.0_rollup@4.60.1_tslib@2.8.1_typescript@6.0.2}/node_modules/tslib/tslib.es6.js +0 -0
@@ -3,6 +3,7 @@
3
3
  * Behavioral parity with react/svelte SeekBar implementations.
4
4
  */
5
5
  import { LitElement } from "lit";
6
+ import { type ThumbnailCue } from "@livepeer-frameworks/player-core";
6
7
  export declare class FwSeekBar extends LitElement {
7
8
  currentTime: number;
8
9
  duration: number;
@@ -12,6 +13,8 @@ export declare class FwSeekBar extends LitElement {
12
13
  seekableStart: number;
13
14
  liveEdge?: number;
14
15
  commitOnRelease: boolean;
16
+ isPlaying: boolean;
17
+ thumbnailCues: ThumbnailCue[];
15
18
  private _hovering;
16
19
  private _dragging;
17
20
  private _dragTime;
@@ -19,8 +22,12 @@ export declare class FwSeekBar extends LitElement {
19
22
  private _hoverTime;
20
23
  private _trackRect;
21
24
  private _activePointerId;
25
+ private _rafId;
26
+ private _rafBase;
22
27
  static styles: import("lit").CSSResult[];
23
28
  disconnectedCallback(): void;
29
+ updated(changed: Map<string, unknown>): void;
30
+ private _syncRaf;
24
31
  private get _effectiveLiveEdge();
25
32
  private get _seekableWindow();
26
33
  private get _displayTime();
@@ -37,10 +44,12 @@ export declare class FwSeekBar extends LitElement {
37
44
  private _onPointerLeave;
38
45
  private _onPointerMove;
39
46
  private _onPointerDown;
47
+ private _didDragMove;
40
48
  private _onGlobalPointerMove;
41
49
  private _onGlobalPointerUp;
42
50
  private _attachDragListeners;
43
51
  private _detachDragListeners;
52
+ private get _thumbnailStyle();
44
53
  protected render(): import("lit").TemplateResult<1>;
45
54
  }
46
55
  declare global {
@@ -3,7 +3,7 @@
3
3
  * Direct port of usePlayerController.ts from player-react.
4
4
  */
5
5
  import type { ReactiveController, ReactiveControllerHost } from "lit";
6
- import { PlayerController, type PlayerControllerConfig, type PlayerState, type StreamState, type StreamInfo, type PlaybackQuality, type ContentEndpoints, type ContentMetadata, type ClassifiedError, type TranslateFn, type I18nConfig } from "@livepeer-frameworks/player-core";
6
+ import { PlayerController, type PlayerControllerConfig, type PlayerState, type StreamState, type StreamInfo, type PlaybackQuality, type ContentEndpoints, type ContentMetadata, type ClassifiedError, type TranslateFn, type I18nConfig, type ThumbnailCue } from "@livepeer-frameworks/player-core";
7
7
  export interface PlayerControllerHostState {
8
8
  state: PlayerState;
9
9
  streamState: StreamState | null;
@@ -60,6 +60,7 @@ export interface PlayerControllerHostState {
60
60
  message: string;
61
61
  timestamp: number;
62
62
  } | null;
63
+ thumbnailCues: ThumbnailCue[];
63
64
  }
64
65
  type HostElement = ReactiveControllerHost & HTMLElement;
65
66
  export declare class PlayerControllerHost implements ReactiveController {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livepeer-frameworks/player-wc",
3
- "version": "0.2.11",
3
+ "version": "0.3.1",
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/esm/index.js",
@@ -26,18 +26,18 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "lit": "^3.3.2",
29
- "@livepeer-frameworks/player-core": "0.2.5"
29
+ "@livepeer-frameworks/player-core": "0.3.1"
30
30
  },
31
31
  "devDependencies": {
32
- "@rollup/plugin-commonjs": "^29.0.0",
32
+ "@rollup/plugin-commonjs": "^29.0.2",
33
33
  "@rollup/plugin-node-resolve": "^16.0.3",
34
- "@rollup/plugin-terser": "^0.4.4",
34
+ "@rollup/plugin-terser": "^1.0.0",
35
35
  "@rollup/plugin-typescript": "^12.3.0",
36
- "@vitest/coverage-v8": "^4.0.18",
37
- "rollup": "^4.59.0",
36
+ "@vitest/coverage-v8": "^4.1.4",
37
+ "rollup": "^4.60.1",
38
38
  "tslib": "^2.8.1",
39
- "typescript": "^5.9.3",
40
- "vitest": "^4.0.18"
39
+ "typescript": "^6.0.2",
40
+ "vitest": "^4.1.4"
41
41
  },
42
42
  "keywords": [
43
43
  "web-components",
@@ -358,6 +358,8 @@ export class FwPlayerControls extends LitElement {
358
358
  .seekableStart=${context.seekableStart}
359
359
  .liveEdge=${context.liveEdge}
360
360
  .commitOnRelease=${context.commitOnRelease}
361
+ .isPlaying=${state.isPlaying}
362
+ .thumbnailCues=${state.thumbnailCues}
361
363
  @fw-seek=${(event: CustomEvent<{ time: number }>) =>
362
364
  this.pc.seek(event.detail.time)}
363
365
  ></fw-seek-bar>
@@ -7,6 +7,7 @@ import { customElement, property, state } from "lit/decorators.js";
7
7
  import { classMap } from "lit/directives/class-map.js";
8
8
  import { styleMap } from "lit/directives/style-map.js";
9
9
  import { sharedStyles } from "../styles/shared-styles.js";
10
+ import { findCueAtTime, type ThumbnailCue } from "@livepeer-frameworks/player-core";
10
11
 
11
12
  interface BufferedSegment {
12
13
  startPercent: number;
@@ -23,6 +24,8 @@ export class FwSeekBar extends LitElement {
23
24
  @property({ type: Number, attribute: "seekable-start" }) seekableStart = 0;
24
25
  @property({ type: Number, attribute: "live-edge" }) liveEdge?: number;
25
26
  @property({ type: Boolean, attribute: "commit-on-release" }) commitOnRelease = false;
27
+ @property({ type: Boolean, attribute: "is-playing" }) isPlaying = false;
28
+ @property({ attribute: false }) thumbnailCues: ThumbnailCue[] = [];
26
29
 
27
30
  @state() private _hovering = false;
28
31
  @state() private _dragging = false;
@@ -32,6 +35,8 @@ export class FwSeekBar extends LitElement {
32
35
 
33
36
  private _trackRect: DOMRect | null = null;
34
37
  private _activePointerId: number | null = null;
38
+ private _rafId = 0;
39
+ private _rafBase = { time: 0, stamp: 0 };
35
40
 
36
41
  static styles = [
37
42
  sharedStyles,
@@ -60,6 +65,56 @@ export class FwSeekBar extends LitElement {
60
65
  disconnectedCallback(): void {
61
66
  super.disconnectedCallback();
62
67
  this._detachDragListeners();
68
+ cancelAnimationFrame(this._rafId);
69
+ }
70
+
71
+ updated(changed: Map<string, unknown>): void {
72
+ if (changed.has("currentTime")) {
73
+ this._rafBase = { time: this.currentTime, stamp: performance.now() };
74
+ }
75
+ if (
76
+ changed.has("isPlaying") ||
77
+ changed.has("disabled") ||
78
+ changed.has("currentTime") ||
79
+ changed.has("duration")
80
+ ) {
81
+ this._syncRaf();
82
+ }
83
+ }
84
+
85
+ private _syncRaf(): void {
86
+ const shouldAnimate = this.isPlaying && !this._dragging && !this.disabled;
87
+
88
+ if (!shouldAnimate) {
89
+ cancelAnimationFrame(this._rafId);
90
+ const el = this.renderRoot.querySelector(".fw-seek-progress") as HTMLElement | null;
91
+ if (el) {
92
+ el.style.transform = `scaleX(${this._progressPercent / 100})`;
93
+ }
94
+ return;
95
+ }
96
+
97
+ cancelAnimationFrame(this._rafId);
98
+ const rangeStart = this.isLive ? this.seekableStart : 0;
99
+ const rangeSize = this.isLive ? this._seekableWindow : this.duration;
100
+
101
+ const animate = () => {
102
+ if (!this.isPlaying || this._dragging || this.disabled) return;
103
+ const interpolated = this._rafBase.time + (performance.now() - this._rafBase.stamp);
104
+ const relative = interpolated - rangeStart;
105
+ const pct =
106
+ Number.isFinite(rangeSize) && rangeSize > 0
107
+ ? Math.min(100, Math.max(0, (relative / rangeSize) * 100))
108
+ : 0;
109
+
110
+ const el = this.renderRoot.querySelector(".fw-seek-progress") as HTMLElement | null;
111
+ if (el) {
112
+ el.style.transform = `scaleX(${pct / 100})`;
113
+ }
114
+ this._rafId = requestAnimationFrame(animate);
115
+ };
116
+
117
+ this._rafId = requestAnimationFrame(animate);
63
118
  }
64
119
 
65
120
  private get _effectiveLiveEdge(): number {
@@ -282,6 +337,7 @@ export class FwSeekBar extends LitElement {
282
337
  this._activePointerId = event.pointerId;
283
338
  this._dragging = true;
284
339
  this._hovering = true;
340
+ this._didDragMove = false;
285
341
 
286
342
  const initialTime = this._getTimeFromClientX(event.clientX);
287
343
  this._updateHover(event.clientX);
@@ -295,11 +351,14 @@ export class FwSeekBar extends LitElement {
295
351
  this._attachDragListeners();
296
352
  };
297
353
 
354
+ private _didDragMove = false;
355
+
298
356
  private _onGlobalPointerMove = (event: PointerEvent) => {
299
357
  if (!this._dragging || this._activePointerId !== event.pointerId) {
300
358
  return;
301
359
  }
302
360
 
361
+ this._didDragMove = true;
303
362
  const time = this._getTimeFromClientX(event.clientX);
304
363
  this._updateHover(event.clientX);
305
364
 
@@ -318,11 +377,14 @@ export class FwSeekBar extends LitElement {
318
377
  if (this.commitOnRelease && this._dragTime != null) {
319
378
  this._emitSeek(this._dragTime);
320
379
  }
380
+ // Non-commitOnRelease: drag moves already emitted seeks, no double-seek on release
321
381
 
322
382
  this._dragging = false;
323
383
  this._dragTime = null;
324
384
  this._activePointerId = null;
385
+ this._didDragMove = false;
325
386
  this._detachDragListeners();
387
+ this._syncRaf();
326
388
  };
327
389
 
328
390
  private _attachDragListeners(): void {
@@ -337,10 +399,25 @@ export class FwSeekBar extends LitElement {
337
399
  window.removeEventListener("pointercancel", this._onGlobalPointerUp);
338
400
  }
339
401
 
402
+ private get _thumbnailStyle(): Record<string, string> | null {
403
+ if (!this.thumbnailCues?.length || !this._hovering || this._dragging) return null;
404
+ const cue = findCueAtTime(this.thumbnailCues, this._hoverTime / 1000);
405
+ if (!cue || cue.width === undefined || cue.height === undefined) return null;
406
+ return {
407
+ backgroundImage: `url(${cue.url})`,
408
+ backgroundPosition: `-${cue.x ?? 0}px -${cue.y ?? 0}px`,
409
+ backgroundSize: "auto",
410
+ width: `${cue.width}px`,
411
+ height: `${cue.height}px`,
412
+ left: `${this._hoverPosition}%`,
413
+ };
414
+ }
415
+
340
416
  protected render() {
341
417
  const progressPercent = this._progressPercent;
342
418
  const showThumb = this._hovering || this._dragging;
343
419
  const canShowTooltip = this.isLive ? this._seekableWindow > 0 : Number.isFinite(this.duration);
420
+ const thumbStyle = this._thumbnailStyle;
344
421
 
345
422
  return html`
346
423
  <div
@@ -380,7 +457,10 @@ export class FwSeekBar extends LitElement {
380
457
  ></div>
381
458
  `
382
459
  )}
383
- <div class="fw-seek-progress" style=${styleMap({ width: `${progressPercent}%` })}></div>
460
+ <div
461
+ class="fw-seek-progress"
462
+ style=${styleMap({ transform: `scaleX(${progressPercent / 100})` })}
463
+ ></div>
384
464
  ${this._hovering && !this._dragging
385
465
  ? html`<div
386
466
  class="fw-seek-hover-line"
@@ -399,6 +479,9 @@ export class FwSeekBar extends LitElement {
399
479
  style=${styleMap({ left: `${progressPercent}%` })}
400
480
  ></div>
401
481
 
482
+ ${thumbStyle
483
+ ? html`<div class="fw-seek-thumbnail" style=${styleMap(thumbStyle)}></div>`
484
+ : nothing}
402
485
  ${this._hovering && !this._dragging && canShowTooltip
403
486
  ? html`
404
487
  <div class="fw-seek-tooltip" style=${styleMap({ left: `${this._hoverPosition}%` })}>
@@ -16,6 +16,7 @@ import {
16
16
  type ClassifiedError,
17
17
  type TranslateFn,
18
18
  type I18nConfig,
19
+ type ThumbnailCue,
19
20
  } from "@livepeer-frameworks/player-core";
20
21
 
21
22
  export interface PlayerControllerHostState {
@@ -60,6 +61,7 @@ export interface PlayerControllerHostState {
60
61
  textTracks: Array<{ id: string; label: string; lang?: string; active: boolean }>;
61
62
  streamInfo: StreamInfo | null;
62
63
  toast: { message: string; timestamp: number } | null;
64
+ thumbnailCues: ThumbnailCue[];
63
65
  }
64
66
 
65
67
  const initialState: PlayerControllerHostState = {
@@ -96,6 +98,7 @@ const initialState: PlayerControllerHostState = {
96
98
  textTracks: [],
97
99
  streamInfo: null,
98
100
  toast: null,
101
+ thumbnailCues: [],
99
102
  };
100
103
 
101
104
  type HostElement = ReactiveControllerHost & HTMLElement;
@@ -391,6 +394,12 @@ export class PlayerControllerHost implements ReactiveController {
391
394
  });
392
395
  })
393
396
  );
397
+
398
+ u.push(
399
+ controller.on("thumbnailCuesChange", ({ cues }) => {
400
+ this.update({ thumbnailCues: cues });
401
+ })
402
+ );
394
403
  }
395
404
 
396
405
  // ---- Event Dispatching ----
@@ -827,10 +827,12 @@ export const sharedStyles = css`
827
827
 
828
828
  .fw-seek-progress {
829
829
  position: absolute;
830
+ width: 100%;
830
831
  height: 100%;
831
832
  border-radius: 9999px;
832
833
  background: hsl(var(--fw-accent));
833
- will-change: width;
834
+ transform-origin: left center;
835
+ will-change: transform;
834
836
  }
835
837
 
836
838
  .fw-seek-hover-line {
@@ -898,6 +900,20 @@ export const sharedStyles = css`
898
900
  transform: translateX(-50%);
899
901
  }
900
902
 
903
+ .fw-seek-thumbnail {
904
+ position: absolute;
905
+ bottom: 100%;
906
+ margin-bottom: 28px;
907
+ border-radius: 4px;
908
+ border: 2px solid hsl(var(--fw-surface-raised));
909
+ box-shadow: 0 4px 12px hsl(var(--fw-shadow-color) / 0.4);
910
+ background-color: hsl(var(--fw-surface-deep));
911
+ background-repeat: no-repeat;
912
+ pointer-events: none;
913
+ transform: translateX(-50%);
914
+ z-index: 10;
915
+ }
916
+
901
917
  /* --- Stats Panel --- */
902
918
  .fw-stats-panel {
903
919
  position: absolute;