@livepeer-frameworks/player-wc 0.2.9 → 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.
- package/dist/esm/components/controls/fw-fullscreen-button.js +1 -1
- package/dist/esm/components/controls/fw-live-badge.js +1 -1
- package/dist/esm/components/controls/fw-play-button.js +1 -1
- package/dist/esm/components/controls/fw-skip-button.js +1 -1
- package/dist/esm/components/controls/fw-time-display.js +1 -1
- package/dist/esm/components/controls/fw-volume-control.js +1 -1
- package/dist/esm/components/fw-context-menu.js +1 -1
- package/dist/esm/components/fw-dev-mode-panel.js +1 -1
- package/dist/esm/components/fw-dvd-logo.js +1 -1
- package/dist/esm/components/fw-error-overlay.js +1 -1
- package/dist/esm/components/fw-idle-screen.js +1 -1
- package/dist/esm/components/fw-loading-screen.js +1 -1
- package/dist/esm/components/fw-loading-spinner.js +1 -1
- package/dist/esm/components/fw-player-controls.js +4 -6
- package/dist/esm/components/fw-player-controls.js.map +1 -1
- package/dist/esm/components/fw-player.js +1 -1
- package/dist/esm/components/fw-seek-bar.js +82 -2
- package/dist/esm/components/fw-seek-bar.js.map +1 -1
- package/dist/esm/components/fw-settings-menu.js +3 -3
- package/dist/esm/components/fw-settings-menu.js.map +1 -1
- package/dist/esm/components/fw-skip-indicator.js +1 -1
- package/dist/esm/components/fw-speed-indicator.js +1 -1
- package/dist/esm/components/fw-stats-panel.js +1 -1
- package/dist/esm/components/fw-stream-state-overlay.js +1 -1
- package/dist/esm/components/fw-subtitle-renderer.js +1 -1
- package/dist/esm/components/fw-thumbnail-overlay.js +1 -1
- package/dist/esm/components/fw-title-overlay.js +1 -1
- package/dist/esm/components/fw-toast.js +1 -1
- package/dist/esm/components/fw-volume-control.js +32 -8
- package/dist/esm/components/fw-volume-control.js.map +1 -1
- package/dist/esm/controllers/player-controller-host.js +4 -0
- package/dist/esm/controllers/player-controller-host.js.map +1 -1
- package/dist/esm/node_modules/.pnpm/{@rollup_plugin-typescript@12.3.0_rollup@4.57.1_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
- package/dist/esm/styles/shared-styles.js +17 -1
- package/dist/esm/styles/shared-styles.js.map +1 -1
- package/dist/fw-player.iife.js +104 -82
- package/dist/types/components/fw-seek-bar.d.ts +9 -0
- package/dist/types/components/fw-volume-control.d.ts +3 -0
- package/dist/types/controllers/player-controller-host.d.ts +2 -1
- package/package.json +8 -8
- package/src/components/fw-player-controls.ts +3 -5
- package/src/components/fw-seek-bar.ts +84 -1
- package/src/components/fw-settings-menu.ts +2 -1
- package/src/components/fw-volume-control.ts +33 -7
- package/src/controllers/player-controller-host.ts +9 -0
- package/src/styles/shared-styles.ts +17 -1
- /package/dist/esm/node_modules/.pnpm/{@rollup_plugin-typescript@12.3.0_rollup@4.57.1_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 {
|
|
@@ -11,10 +11,13 @@ export declare class FwVolumeControl extends LitElement {
|
|
|
11
11
|
private _hasAudio;
|
|
12
12
|
private _activePointerId;
|
|
13
13
|
private _activeSliderTarget;
|
|
14
|
+
private _boundStream;
|
|
15
|
+
private _onStreamTrackChange;
|
|
14
16
|
static styles: import("lit").CSSResult[];
|
|
15
17
|
private get _expanded();
|
|
16
18
|
disconnectedCallback(): void;
|
|
17
19
|
protected updated(): void;
|
|
20
|
+
private _unbindStreamListeners;
|
|
18
21
|
private _updateHasAudio;
|
|
19
22
|
private _setVolumeFromClientX;
|
|
20
23
|
private _beginDragInteraction;
|
|
@@ -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.
|
|
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.
|
|
29
|
+
"@livepeer-frameworks/player-core": "0.3.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@rollup/plugin-commonjs": "^29.0.
|
|
32
|
+
"@rollup/plugin-commonjs": "^29.0.2",
|
|
33
33
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
34
|
-
"@rollup/plugin-terser": "^0.
|
|
34
|
+
"@rollup/plugin-terser": "^1.0.0",
|
|
35
35
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
36
|
-
"@vitest/coverage-v8": "^4.
|
|
37
|
-
"rollup": "^4.
|
|
36
|
+
"@vitest/coverage-v8": "^4.1.4",
|
|
37
|
+
"rollup": "^4.60.1",
|
|
38
38
|
"tslib": "^2.8.1",
|
|
39
|
-
"typescript": "^
|
|
40
|
-
"vitest": "^4.
|
|
39
|
+
"typescript": "^6.0.2",
|
|
40
|
+
"vitest": "^4.1.4"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"web-components",
|
|
@@ -223,11 +223,7 @@ export class FwPlayerControls extends LitElement {
|
|
|
223
223
|
const isWebRTC = isMediaStreamSource(state.videoElement);
|
|
224
224
|
|
|
225
225
|
const allowMediaStreamDvr =
|
|
226
|
-
isMediaStreamSource(state.videoElement) &&
|
|
227
|
-
bufferWindowMs !== undefined &&
|
|
228
|
-
bufferWindowMs > 0 &&
|
|
229
|
-
sourceType !== "whep" &&
|
|
230
|
-
sourceType !== "webrtc";
|
|
226
|
+
isMediaStreamSource(state.videoElement) && bufferWindowMs !== undefined && bufferWindowMs > 0;
|
|
231
227
|
|
|
232
228
|
const calculatedRange = calculateSeekableRange({
|
|
233
229
|
isLive,
|
|
@@ -362,6 +358,8 @@ export class FwPlayerControls extends LitElement {
|
|
|
362
358
|
.seekableStart=${context.seekableStart}
|
|
363
359
|
.liveEdge=${context.liveEdge}
|
|
364
360
|
.commitOnRelease=${context.commitOnRelease}
|
|
361
|
+
.isPlaying=${state.isPlaying}
|
|
362
|
+
.thumbnailCues=${state.thumbnailCues}
|
|
365
363
|
@fw-seek=${(event: CustomEvent<{ time: number }>) =>
|
|
366
364
|
this.pc.seek(event.detail.time)}
|
|
367
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
|
|
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}%` })}>
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
supportsPlaybackRate as coreSupportsPlaybackRate,
|
|
12
12
|
getAvailableLocales,
|
|
13
13
|
getLocaleDisplayName,
|
|
14
|
+
formatQualityLabel,
|
|
14
15
|
} from "@livepeer-frameworks/player-core";
|
|
15
16
|
import type { PlaybackMode, FwLocale } from "@livepeer-frameworks/player-core";
|
|
16
17
|
import type { PlayerControllerHost } from "../controllers/player-controller-host.js";
|
|
@@ -152,7 +153,7 @@ export class FwSettingsMenu extends LitElement {
|
|
|
152
153
|
.filter(([, track]) => track?.type === "video")
|
|
153
154
|
.map(([id, track]) => ({
|
|
154
155
|
id,
|
|
155
|
-
label: track.
|
|
156
|
+
label: formatQualityLabel(track.width, track.height, track.bps),
|
|
156
157
|
width: track.width,
|
|
157
158
|
height: track.height,
|
|
158
159
|
bitrate: track.bps,
|
|
@@ -20,6 +20,8 @@ export class FwVolumeControl extends LitElement {
|
|
|
20
20
|
|
|
21
21
|
private _activePointerId: number | null = null;
|
|
22
22
|
private _activeSliderTarget: HTMLElement | null = null;
|
|
23
|
+
private _boundStream: MediaStream | null = null;
|
|
24
|
+
private _onStreamTrackChange: (() => void) | null = null;
|
|
23
25
|
|
|
24
26
|
static styles = [
|
|
25
27
|
sharedStyles,
|
|
@@ -69,28 +71,52 @@ export class FwVolumeControl extends LitElement {
|
|
|
69
71
|
disconnectedCallback(): void {
|
|
70
72
|
super.disconnectedCallback();
|
|
71
73
|
this._endDragInteraction();
|
|
74
|
+
this._unbindStreamListeners();
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
protected updated(): void {
|
|
75
78
|
this._updateHasAudio();
|
|
76
79
|
}
|
|
77
80
|
|
|
78
|
-
private
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
this._hasAudio = mistHasAudio;
|
|
83
|
-
return;
|
|
81
|
+
private _unbindStreamListeners(): void {
|
|
82
|
+
if (this._boundStream && this._onStreamTrackChange) {
|
|
83
|
+
this._boundStream.removeEventListener("addtrack", this._onStreamTrackChange);
|
|
84
|
+
this._boundStream.removeEventListener("removetrack", this._onStreamTrackChange);
|
|
84
85
|
}
|
|
86
|
+
this._boundStream = null;
|
|
87
|
+
this._onStreamTrackChange = null;
|
|
88
|
+
}
|
|
85
89
|
|
|
90
|
+
private _updateHasAudio(): void {
|
|
86
91
|
const video = this.pc?.s.videoElement;
|
|
87
92
|
if (!video) {
|
|
93
|
+
this._unbindStreamListeners();
|
|
88
94
|
this._hasAudio = true;
|
|
89
95
|
return;
|
|
90
96
|
}
|
|
91
97
|
|
|
98
|
+
// MediaStream: bind track change listeners (WebRTC tracks arrive async)
|
|
92
99
|
if (video.srcObject instanceof MediaStream) {
|
|
93
|
-
|
|
100
|
+
const stream = video.srcObject;
|
|
101
|
+
if (stream !== this._boundStream) {
|
|
102
|
+
this._unbindStreamListeners();
|
|
103
|
+
this._boundStream = stream;
|
|
104
|
+
this._onStreamTrackChange = () => {
|
|
105
|
+
this._hasAudio = stream.getAudioTracks().length > 0;
|
|
106
|
+
};
|
|
107
|
+
stream.addEventListener("addtrack", this._onStreamTrackChange);
|
|
108
|
+
stream.addEventListener("removetrack", this._onStreamTrackChange);
|
|
109
|
+
}
|
|
110
|
+
this._hasAudio = stream.getAudioTracks().length > 0;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this._unbindStreamListeners();
|
|
115
|
+
|
|
116
|
+
// Fallback: metadata for non-MediaStream sources.
|
|
117
|
+
const mistHasAudio = this.pc?.s.streamState?.streamInfo?.hasAudio;
|
|
118
|
+
if (mistHasAudio !== undefined) {
|
|
119
|
+
this._hasAudio = mistHasAudio;
|
|
94
120
|
return;
|
|
95
121
|
}
|
|
96
122
|
|
|
@@ -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
|
-
|
|
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;
|