@livepeer-frameworks/player-wc 0.1.1 → 0.1.3

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 (116) hide show
  1. package/dist/cjs/components/fw-dev-mode-panel.js +845 -212
  2. package/dist/cjs/components/fw-dev-mode-panel.js.map +1 -1
  3. package/dist/cjs/components/fw-dvd-logo.js +211 -0
  4. package/dist/cjs/components/fw-dvd-logo.js.map +1 -0
  5. package/dist/cjs/components/fw-idle-screen.js +641 -97
  6. package/dist/cjs/components/fw-idle-screen.js.map +1 -1
  7. package/dist/cjs/components/fw-loading-screen.js +513 -0
  8. package/dist/cjs/components/fw-loading-screen.js.map +1 -0
  9. package/dist/cjs/components/fw-player-controls.js +347 -173
  10. package/dist/cjs/components/fw-player-controls.js.map +1 -1
  11. package/dist/cjs/components/fw-player.js +460 -60
  12. package/dist/cjs/components/fw-player.js.map +1 -1
  13. package/dist/cjs/components/fw-seek-bar.js +292 -142
  14. package/dist/cjs/components/fw-seek-bar.js.map +1 -1
  15. package/dist/cjs/components/fw-settings-menu.js +191 -81
  16. package/dist/cjs/components/fw-settings-menu.js.map +1 -1
  17. package/dist/cjs/components/fw-stats-panel.js +134 -70
  18. package/dist/cjs/components/fw-stats-panel.js.map +1 -1
  19. package/dist/cjs/components/fw-stream-state-overlay.js +338 -0
  20. package/dist/cjs/components/fw-stream-state-overlay.js.map +1 -0
  21. package/dist/cjs/components/fw-subtitle-renderer.js +174 -27
  22. package/dist/cjs/components/fw-subtitle-renderer.js.map +1 -1
  23. package/dist/cjs/components/fw-thumbnail-overlay.js +161 -0
  24. package/dist/cjs/components/fw-thumbnail-overlay.js.map +1 -0
  25. package/dist/cjs/components/fw-volume-control.js +150 -69
  26. package/dist/cjs/components/fw-volume-control.js.map +1 -1
  27. package/dist/cjs/components/shared/hitmarker-audio.js +76 -0
  28. package/dist/cjs/components/shared/hitmarker-audio.js.map +1 -0
  29. package/dist/cjs/constants/media-assets.js +11 -0
  30. package/dist/cjs/constants/media-assets.js.map +1 -0
  31. package/dist/cjs/controllers/player-controller-host.js +28 -1
  32. package/dist/cjs/controllers/player-controller-host.js.map +1 -1
  33. package/dist/cjs/define.js +8 -0
  34. package/dist/cjs/define.js.map +1 -1
  35. package/dist/cjs/icons/index.js +27 -0
  36. package/dist/cjs/icons/index.js.map +1 -1
  37. package/dist/cjs/index.js +20 -0
  38. package/dist/cjs/index.js.map +1 -1
  39. package/dist/esm/components/fw-dev-mode-panel.js +846 -213
  40. package/dist/esm/components/fw-dev-mode-panel.js.map +1 -1
  41. package/dist/esm/components/fw-dvd-logo.js +211 -0
  42. package/dist/esm/components/fw-dvd-logo.js.map +1 -0
  43. package/dist/esm/components/fw-idle-screen.js +643 -99
  44. package/dist/esm/components/fw-idle-screen.js.map +1 -1
  45. package/dist/esm/components/fw-loading-screen.js +513 -0
  46. package/dist/esm/components/fw-loading-screen.js.map +1 -0
  47. package/dist/esm/components/fw-player-controls.js +348 -174
  48. package/dist/esm/components/fw-player-controls.js.map +1 -1
  49. package/dist/esm/components/fw-player.js +460 -60
  50. package/dist/esm/components/fw-player.js.map +1 -1
  51. package/dist/esm/components/fw-seek-bar.js +293 -143
  52. package/dist/esm/components/fw-seek-bar.js.map +1 -1
  53. package/dist/esm/components/fw-settings-menu.js +192 -82
  54. package/dist/esm/components/fw-settings-menu.js.map +1 -1
  55. package/dist/esm/components/fw-stats-panel.js +135 -71
  56. package/dist/esm/components/fw-stats-panel.js.map +1 -1
  57. package/dist/esm/components/fw-stream-state-overlay.js +338 -0
  58. package/dist/esm/components/fw-stream-state-overlay.js.map +1 -0
  59. package/dist/esm/components/fw-subtitle-renderer.js +175 -28
  60. package/dist/esm/components/fw-subtitle-renderer.js.map +1 -1
  61. package/dist/esm/components/fw-thumbnail-overlay.js +161 -0
  62. package/dist/esm/components/fw-thumbnail-overlay.js.map +1 -0
  63. package/dist/esm/components/fw-volume-control.js +150 -69
  64. package/dist/esm/components/fw-volume-control.js.map +1 -1
  65. package/dist/esm/components/shared/hitmarker-audio.js +74 -0
  66. package/dist/esm/components/shared/hitmarker-audio.js.map +1 -0
  67. package/dist/esm/constants/media-assets.js +8 -0
  68. package/dist/esm/constants/media-assets.js.map +1 -0
  69. package/dist/esm/controllers/player-controller-host.js +28 -1
  70. package/dist/esm/controllers/player-controller-host.js.map +1 -1
  71. package/dist/esm/define.js +8 -0
  72. package/dist/esm/define.js.map +1 -1
  73. package/dist/esm/icons/index.js +26 -2
  74. package/dist/esm/icons/index.js.map +1 -1
  75. package/dist/esm/index.js +4 -0
  76. package/dist/esm/index.js.map +1 -1
  77. package/dist/fw-player.iife.js +2072 -880
  78. package/dist/types/components/fw-dev-mode-panel.d.ts +36 -9
  79. package/dist/types/components/fw-dvd-logo.d.ts +29 -0
  80. package/dist/types/components/fw-idle-screen.d.ts +36 -0
  81. package/dist/types/components/fw-loading-screen.d.ts +36 -0
  82. package/dist/types/components/fw-player-controls.d.ts +21 -6
  83. package/dist/types/components/fw-player.d.ts +28 -1
  84. package/dist/types/components/fw-seek-bar.d.ts +31 -14
  85. package/dist/types/components/fw-settings-menu.d.ts +15 -1
  86. package/dist/types/components/fw-stats-panel.d.ts +4 -4
  87. package/dist/types/components/fw-stream-state-overlay.d.ts +20 -0
  88. package/dist/types/components/fw-subtitle-renderer.d.ts +33 -2
  89. package/dist/types/components/fw-thumbnail-overlay.d.ts +17 -0
  90. package/dist/types/components/fw-volume-control.d.ts +11 -4
  91. package/dist/types/components/shared/hitmarker-audio.d.ts +1 -0
  92. package/dist/types/constants/media-assets.d.ts +5 -0
  93. package/dist/types/controllers/player-controller-host.d.ts +14 -1
  94. package/dist/types/iife-entry.d.ts +4 -0
  95. package/dist/types/index.d.ts +4 -0
  96. package/package.json +3 -3
  97. package/src/components/fw-dev-mode-panel.ts +929 -228
  98. package/src/components/fw-dvd-logo.ts +233 -0
  99. package/src/components/fw-idle-screen.ts +680 -100
  100. package/src/components/fw-loading-screen.ts +540 -0
  101. package/src/components/fw-player-controls.ts +435 -176
  102. package/src/components/fw-player.ts +505 -57
  103. package/src/components/fw-seek-bar.ts +336 -143
  104. package/src/components/fw-settings-menu.ts +208 -85
  105. package/src/components/fw-stats-panel.ts +150 -77
  106. package/src/components/fw-stream-state-overlay.ts +331 -0
  107. package/src/components/fw-subtitle-renderer.ts +216 -28
  108. package/src/components/fw-thumbnail-overlay.ts +148 -0
  109. package/src/components/fw-volume-control.ts +166 -66
  110. package/src/components/shared/hitmarker-audio.ts +92 -0
  111. package/src/constants/media-assets.ts +7 -0
  112. package/src/controllers/player-controller-host.ts +29 -2
  113. package/src/define.ts +8 -0
  114. package/src/iife-entry.ts +4 -0
  115. package/src/index.ts +4 -0
  116. package/dist/fw-player.iife.js.map +0 -1
@@ -4,131 +4,382 @@ import { property, state, customElement } from 'lit/decorators.js';
4
4
  import { classMap } from 'lit/directives/class-map.js';
5
5
  import { sharedStyles } from '../styles/shared-styles.js';
6
6
  import { utilityStyles } from '../styles/utility-styles.js';
7
- import { pauseIcon, playIcon, settingsIcon, fullscreenExitIcon, fullscreenIcon } from '../icons/index.js';
7
+ import { pauseIcon, playIcon, skipBackIcon, skipForwardIcon, seekToLiveIcon, statsIcon, settingsIcon, fullscreenExitIcon, fullscreenIcon } from '../icons/index.js';
8
+ import { isLiveContent, isMediaStreamSource, calculateSeekableRange, canSeekStream, calculateLiveThresholds, calculateIsNearLive, formatTimeDisplay } from '@livepeer-frameworks/player-core';
8
9
 
9
10
  let FwPlayerControls = class FwPlayerControls extends LitElement {
10
11
  constructor() {
11
12
  super(...arguments);
12
13
  this.playbackMode = "auto";
13
14
  this.isContentLive = false;
15
+ this.showStatsButton = false;
14
16
  this.isStatsOpen = false;
15
17
  this._settingsOpen = false;
18
+ this._isNearLiveState = true;
19
+ this._buffered = null;
20
+ this._boundVideo = null;
21
+ this._onBufferedUpdate = null;
22
+ this._onWindowClick = (event) => {
23
+ const path = event.composedPath();
24
+ const insideControls = path.some((entry) => {
25
+ if (!(entry instanceof HTMLElement)) {
26
+ return false;
27
+ }
28
+ return (entry.classList.contains("fw-settings-anchor") ||
29
+ entry.classList.contains("fw-settings-menu"));
30
+ });
31
+ if (!insideControls) {
32
+ this._settingsOpen = false;
33
+ }
34
+ };
16
35
  }
17
- _formatTime(seconds) {
18
- if (!isFinite(seconds) || isNaN(seconds))
19
- return "0:00";
20
- const abs = Math.abs(Math.floor(seconds));
21
- const h = Math.floor(abs / 3600);
22
- const m = Math.floor((abs % 3600) / 60);
23
- const s = abs % 60;
24
- if (h > 0)
25
- return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
26
- return `${m}:${String(s).padStart(2, "0")}`;
36
+ connectedCallback() {
37
+ super.connectedCallback();
27
38
  }
28
- get _isNearLive() {
29
- if (!this.isContentLive)
30
- return false;
31
- const { currentTime, duration } = this.pc.s;
32
- if (!isFinite(duration) || duration <= 0)
33
- return true;
34
- return duration - currentTime < 10;
39
+ disconnectedCallback() {
40
+ super.disconnectedCallback();
41
+ this._unbindVideoEvents();
42
+ this._detachWindowClickListener();
43
+ }
44
+ updated(changed) {
45
+ this._bindVideoEvents();
46
+ this._reconcileNearLiveState();
47
+ if (changed.has("_settingsOpen")) {
48
+ if (this._settingsOpen) {
49
+ this._attachWindowClickListener();
50
+ }
51
+ else {
52
+ this._detachWindowClickListener();
53
+ }
54
+ }
55
+ }
56
+ _bindVideoEvents() {
57
+ const video = this.pc?.s.videoElement ?? null;
58
+ if (video === this._boundVideo) {
59
+ return;
60
+ }
61
+ this._unbindVideoEvents();
62
+ this._boundVideo = video;
63
+ if (!video) {
64
+ this._buffered = null;
65
+ return;
66
+ }
67
+ const updateBuffered = () => {
68
+ this._buffered = this.pc.getBufferedRanges() ?? video.buffered;
69
+ };
70
+ updateBuffered();
71
+ video.addEventListener("progress", updateBuffered);
72
+ video.addEventListener("loadeddata", updateBuffered);
73
+ this._onBufferedUpdate = updateBuffered;
74
+ }
75
+ _unbindVideoEvents() {
76
+ if (!this._boundVideo) {
77
+ return;
78
+ }
79
+ const updateBuffered = this._onBufferedUpdate;
80
+ if (updateBuffered) {
81
+ this._boundVideo.removeEventListener("progress", updateBuffered);
82
+ this._boundVideo.removeEventListener("loadeddata", updateBuffered);
83
+ }
84
+ this._boundVideo = null;
85
+ this._onBufferedUpdate = null;
86
+ }
87
+ _attachWindowClickListener() {
88
+ window.setTimeout(() => {
89
+ if (!this._settingsOpen) {
90
+ return;
91
+ }
92
+ window.addEventListener("click", this._onWindowClick);
93
+ }, 0);
94
+ }
95
+ _detachWindowClickListener() {
96
+ window.removeEventListener("click", this._onWindowClick);
97
+ }
98
+ _deriveBufferWindowMs(tracks) {
99
+ if (!tracks) {
100
+ return undefined;
101
+ }
102
+ const trackValues = Object.values(tracks);
103
+ if (trackValues.length === 0) {
104
+ return undefined;
105
+ }
106
+ const firstmsValues = trackValues
107
+ .map((track) => track.firstms)
108
+ .filter((value) => typeof value === "number");
109
+ const lastmsValues = trackValues
110
+ .map((track) => track.lastms)
111
+ .filter((value) => typeof value === "number");
112
+ if (firstmsValues.length === 0 || lastmsValues.length === 0) {
113
+ return undefined;
114
+ }
115
+ const firstms = Math.max(...firstmsValues);
116
+ const lastms = Math.min(...lastmsValues);
117
+ const window = lastms - firstms;
118
+ if (!Number.isFinite(window) || window <= 0) {
119
+ return undefined;
120
+ }
121
+ return window;
122
+ }
123
+ _getSeekingContext() {
124
+ const state = this.pc.s;
125
+ const controller = this.pc.getController();
126
+ const sourceType = state.currentSourceInfo?.type;
127
+ const mistStreamInfo = state.streamState?.streamInfo;
128
+ const isLive = isLiveContent(this.isContentLive, mistStreamInfo, state.duration);
129
+ const bufferWindowMs = mistStreamInfo?.meta?.buffer_window ??
130
+ this._deriveBufferWindowMs(mistStreamInfo?.meta?.tracks);
131
+ const isWebRTC = isMediaStreamSource(state.videoElement);
132
+ const allowMediaStreamDvr = isMediaStreamSource(state.videoElement) &&
133
+ bufferWindowMs !== undefined &&
134
+ bufferWindowMs > 0 &&
135
+ sourceType !== "whep" &&
136
+ sourceType !== "webrtc";
137
+ const calculatedRange = calculateSeekableRange({
138
+ isLive,
139
+ video: state.videoElement,
140
+ mistStreamInfo,
141
+ currentTime: state.currentTime,
142
+ duration: state.duration,
143
+ allowMediaStreamDvr,
144
+ });
145
+ const controllerSeekableStart = this.pc.getSeekableStart();
146
+ const controllerLiveEdge = this.pc.getLiveEdge();
147
+ const useControllerRange = Number.isFinite(controllerSeekableStart) &&
148
+ Number.isFinite(controllerLiveEdge) &&
149
+ controllerLiveEdge >= controllerSeekableStart &&
150
+ (controllerLiveEdge > 0 || controllerSeekableStart > 0);
151
+ const seekableStart = useControllerRange
152
+ ? controllerSeekableStart
153
+ : calculatedRange.seekableStart;
154
+ const liveEdge = useControllerRange ? controllerLiveEdge : calculatedRange.liveEdge;
155
+ const hasDvrWindow = isLive &&
156
+ Number.isFinite(liveEdge) &&
157
+ Number.isFinite(seekableStart) &&
158
+ liveEdge > seekableStart;
159
+ const baseCanSeek = controller?.canSeekStream?.() ??
160
+ canSeekStream({
161
+ video: state.videoElement,
162
+ isLive,
163
+ duration: state.duration,
164
+ bufferWindowMs,
165
+ });
166
+ const liveThresholds = calculateLiveThresholds(sourceType, isWebRTC, bufferWindowMs);
167
+ return {
168
+ mistStreamInfo,
169
+ isLive,
170
+ sourceType,
171
+ seekableStart,
172
+ liveEdge,
173
+ hasDvrWindow,
174
+ canSeek: baseCanSeek && (!isLive || hasDvrWindow),
175
+ commitOnRelease: isLive,
176
+ liveThresholds,
177
+ };
178
+ }
179
+ _reconcileNearLiveState() {
180
+ const context = this._getSeekingContext();
181
+ if (!context.isLive) {
182
+ if (!this._isNearLiveState) {
183
+ this._isNearLiveState = true;
184
+ }
185
+ return;
186
+ }
187
+ const next = calculateIsNearLive(this.pc.s.currentTime, context.liveEdge, context.liveThresholds, this._isNearLiveState);
188
+ if (next !== this._isNearLiveState) {
189
+ this._isNearLiveState = next;
190
+ }
191
+ }
192
+ _handleModeChange(event) {
193
+ const { mode } = event.detail;
194
+ this.dispatchEvent(new CustomEvent("fw-mode-change", {
195
+ detail: { mode },
196
+ bubbles: true,
197
+ composed: true,
198
+ }));
35
199
  }
36
200
  render() {
37
- const s = this.pc.s;
38
- const disabled = !s.videoElement;
201
+ const state = this.pc.s;
202
+ const disabled = !state.videoElement;
203
+ const context = this._getSeekingContext();
204
+ const timeDisplay = formatTimeDisplay({
205
+ isLive: context.isLive,
206
+ currentTime: state.currentTime,
207
+ duration: state.duration,
208
+ liveEdge: context.liveEdge,
209
+ seekableStart: context.seekableStart,
210
+ unixoffset: context.mistStreamInfo?.unixoffset,
211
+ });
212
+ const liveButtonDisabled = !context.hasDvrWindow || this._isNearLiveState;
39
213
  return html `
40
214
  <div
41
215
  class=${classMap({
42
- "controls-wrapper": true,
43
- "controls-wrapper--visible": s.shouldShowControls,
216
+ "fw-player-surface": true,
44
217
  "fw-controls-wrapper": true,
218
+ "fw-controls-wrapper--visible": state.shouldShowControls,
219
+ "fw-controls-wrapper--hidden": !state.shouldShowControls,
45
220
  })}
46
221
  >
47
- <div class="bar fw-control-bar">
48
- <!-- Seek bar -->
49
- <fw-seek-bar
50
- .currentTime=${s.currentTime}
51
- .duration=${s.duration}
52
- .disabled=${disabled}
53
- .isLive=${this.isContentLive}
54
- @fw-seek=${(e) => this.pc.seek(e.detail.time)}
55
- ></fw-seek-bar>
56
-
57
- <!-- Button row -->
58
- <div class="row">
59
- <div class="group fw-control-group">
60
- <!-- Play/Pause -->
61
- <button
62
- class="btn fw-btn-flush"
63
- type="button"
64
- ?disabled=${disabled}
65
- @click=${() => this.pc.togglePlay()}
66
- aria-label="${s.isPlaying ? "Pause" : "Play"}"
67
- >
68
- ${s.isPlaying ? pauseIcon(18) : playIcon(18)}
69
- </button>
222
+ <div class="fw-control-bar" @click=${(event) => event.stopPropagation()}>
223
+ ${context.canSeek
224
+ ? html `
225
+ <div class="fw-seek-wrapper">
226
+ <fw-seek-bar
227
+ .currentTime=${state.currentTime}
228
+ .duration=${state.duration}
229
+ .buffered=${this._buffered}
230
+ .disabled=${disabled}
231
+ .isLive=${context.isLive}
232
+ .seekableStart=${context.seekableStart}
233
+ .liveEdge=${context.liveEdge}
234
+ .commitOnRelease=${context.commitOnRelease}
235
+ @fw-seek=${(event) => this.pc.seek(event.detail.time)}
236
+ ></fw-seek-bar>
237
+ </div>
238
+ `
239
+ : nothing}
70
240
 
71
- <!-- Volume -->
72
- <fw-volume-control .pc=${this.pc}></fw-volume-control>
241
+ <div class="fw-controls-row">
242
+ <div class="fw-controls-left">
243
+ <div class="fw-control-group">
244
+ <button
245
+ type="button"
246
+ class="fw-btn-flush"
247
+ ?disabled=${disabled}
248
+ aria-label=${state.isPlaying ? "Pause" : "Play"}
249
+ @click=${() => this.pc.togglePlay()}
250
+ >
251
+ ${state.isPlaying ? pauseIcon(18) : playIcon(18)}
252
+ </button>
73
253
 
74
- <!-- Time display -->
75
- ${!this.isContentLive
254
+ ${context.canSeek
76
255
  ? html `
77
- <span class="time fw-time-display">
78
- ${this._formatTime(s.currentTime)} / ${this._formatTime(s.duration)}
79
- </span>
80
- `
256
+ <button
257
+ type="button"
258
+ class="fw-btn-flush hidden sm:flex"
259
+ ?disabled=${disabled}
260
+ aria-label="Skip back 10 seconds"
261
+ @click=${() => this.pc.seekBy(-10)}
262
+ >
263
+ ${skipBackIcon(16)}
264
+ </button>
265
+ <button
266
+ type="button"
267
+ class="fw-btn-flush hidden sm:flex"
268
+ ?disabled=${disabled}
269
+ aria-label="Skip forward 10 seconds"
270
+ @click=${() => this.pc.seekBy(10)}
271
+ >
272
+ ${skipForwardIcon(16)}
273
+ </button>
274
+ `
81
275
  : nothing}
276
+ </div>
277
+
278
+ <div class="fw-control-group">
279
+ <fw-volume-control .pc=${this.pc}></fw-volume-control>
280
+ </div>
281
+
282
+ <div class="fw-control-group">
283
+ <span class="fw-time-display">${timeDisplay}</span>
284
+ </div>
82
285
 
83
- <!-- Live badge -->
84
- ${this.isContentLive
286
+ ${context.isLive
85
287
  ? html `
86
- <button
87
- class=${classMap({
88
- "live-badge": true,
288
+ <div class="fw-control-group">
289
+ <button
290
+ type="button"
291
+ @click=${() => this.pc.jumpToLive()}
292
+ ?disabled=${liveButtonDisabled}
293
+ class=${classMap({
89
294
  "fw-live-badge": true,
90
- "live-badge--active": this._isNearLive,
91
- "fw-live-badge--active": this._isNearLive,
92
- "live-badge--behind": !this._isNearLive,
93
- "fw-live-badge--behind": !this._isNearLive,
295
+ "fw-live-badge--active": liveButtonDisabled,
296
+ "fw-live-badge--behind": !liveButtonDisabled,
94
297
  })}
95
- type="button"
96
- @click=${() => this.pc.jumpToLive()}
97
- aria-label="Jump to live"
98
- >
99
- <span class="live-dot"></span>
100
- LIVE
101
- </button>
298
+ title=${!context.hasDvrWindow
299
+ ? "Live only"
300
+ : this._isNearLiveState
301
+ ? "At live edge"
302
+ : "Jump to live"}
303
+ >
304
+ LIVE
305
+ ${!this._isNearLiveState && context.hasDvrWindow
306
+ ? seekToLiveIcon(10)
307
+ : nothing}
308
+ </button>
309
+ </div>
102
310
  `
103
311
  : nothing}
104
312
  </div>
105
313
 
106
- <div class="group fw-control-group">
107
- <!-- Settings -->
108
- <div class="settings-anchor">
314
+ <div class="fw-controls-right">
315
+ ${this.showStatsButton
316
+ ? html `
317
+ <div class="fw-control-group">
318
+ <button
319
+ type="button"
320
+ class=${classMap({
321
+ "fw-btn-flush": true,
322
+ "fw-btn-flush--active": this.isStatsOpen,
323
+ })}
324
+ aria-label="Toggle stats"
325
+ title="Stats"
326
+ @click=${() => this.dispatchEvent(new CustomEvent("fw-stats-toggle", {
327
+ bubbles: true,
328
+ composed: true,
329
+ }))}
330
+ >
331
+ ${statsIcon(16)}
332
+ </button>
333
+ </div>
334
+ `
335
+ : nothing}
336
+
337
+ <div class="fw-control-group fw-settings-anchor">
109
338
  <button
110
- class="btn fw-btn-flush"
111
339
  type="button"
340
+ class=${classMap({
341
+ "fw-btn-flush": true,
342
+ group: true,
343
+ "fw-btn-flush--active": this._settingsOpen,
344
+ })}
345
+ aria-label="Settings"
346
+ title="Settings"
347
+ ?disabled=${disabled}
112
348
  @click=${() => {
349
+ if (disabled) {
350
+ return;
351
+ }
113
352
  this._settingsOpen = !this._settingsOpen;
114
353
  }}
115
- aria-label="Settings"
116
354
  >
117
- ${settingsIcon(16)}
355
+ <span class="transition-transform group-hover:rotate-90"
356
+ >${settingsIcon(16)}</span
357
+ >
118
358
  </button>
119
- <fw-settings-menu .pc=${this.pc} .open=${this._settingsOpen}></fw-settings-menu>
359
+
360
+ <fw-settings-menu
361
+ .pc=${this.pc}
362
+ .open=${this._settingsOpen}
363
+ .playbackMode=${this.playbackMode}
364
+ .isContentLive=${this.isContentLive}
365
+ @fw-close=${() => {
366
+ this._settingsOpen = false;
367
+ }}
368
+ @fw-mode-change=${this._handleModeChange}
369
+ ></fw-settings-menu>
120
370
  </div>
121
371
 
122
- <!-- Fullscreen -->
123
- <button
124
- class="btn fw-btn-flush"
125
- type="button"
126
- ?disabled=${disabled}
127
- @click=${() => this.pc.toggleFullscreen()}
128
- aria-label="${s.isFullscreen ? "Exit fullscreen" : "Fullscreen"}"
129
- >
130
- ${s.isFullscreen ? fullscreenExitIcon(16) : fullscreenIcon(16)}
131
- </button>
372
+ <div class="fw-control-group">
373
+ <button
374
+ type="button"
375
+ class="fw-btn-flush"
376
+ ?disabled=${disabled}
377
+ aria-label=${state.isFullscreen ? "Exit fullscreen" : "Fullscreen"}
378
+ @click=${() => this.pc.toggleFullscreen()}
379
+ >
380
+ ${state.isFullscreen ? fullscreenExitIcon(16) : fullscreenIcon(16)}
381
+ </button>
382
+ </div>
132
383
  </div>
133
384
  </div>
134
385
  </div>
@@ -143,94 +394,8 @@ FwPlayerControls.styles = [
143
394
  :host {
144
395
  display: contents;
145
396
  }
146
- .controls-wrapper {
147
- position: absolute;
148
- bottom: 0;
149
- left: 0;
150
- right: 0;
151
- z-index: 20;
152
- background: linear-gradient(to top, rgb(0 0 0 / 0.7), transparent);
153
- padding: 2rem 0.75rem 0.5rem;
154
- opacity: 0;
155
- transition: opacity 200ms ease;
156
- pointer-events: none;
157
- }
158
- .controls-wrapper--visible {
159
- opacity: 1;
160
- pointer-events: auto;
161
- }
162
- .bar {
163
- display: flex;
164
- flex-direction: column;
165
- gap: 0.25rem;
166
- }
167
- .row {
168
- display: flex;
169
- align-items: center;
170
- justify-content: space-between;
171
- gap: 0.25rem;
172
- }
173
- .group {
174
- display: flex;
175
- align-items: center;
176
- gap: 0.125rem;
177
- }
178
- .btn {
179
- display: flex;
180
- align-items: center;
181
- justify-content: center;
182
- width: 2rem;
183
- height: 2rem;
184
- background: none;
185
- border: none;
186
- color: rgb(255 255 255 / 0.8);
187
- cursor: pointer;
188
- padding: 0;
189
- border-radius: 0.25rem;
190
- transition: color 150ms;
191
- }
192
- .btn:hover {
193
- color: white;
194
- }
195
- .btn:disabled {
196
- opacity: 0.4;
197
- cursor: not-allowed;
198
- }
199
- .time {
200
- font-size: 0.6875rem;
201
- color: rgb(255 255 255 / 0.7);
202
- font-variant-numeric: tabular-nums;
203
- padding: 0 0.375rem;
204
- white-space: nowrap;
205
- }
206
- .live-badge {
207
- display: inline-flex;
208
- align-items: center;
209
- gap: 0.375rem;
210
- padding: 0.125rem 0.5rem;
211
- border-radius: 0.25rem;
212
- font-size: 0.6875rem;
213
- font-weight: 600;
214
- text-transform: uppercase;
215
- letter-spacing: 0.025em;
216
- cursor: pointer;
217
- border: none;
218
- background: none;
219
- transition: color 150ms;
220
- }
221
- .live-badge--active {
222
- color: hsl(var(--tn-red, 348 74% 64%));
223
- }
224
- .live-badge--behind {
225
- color: rgb(255 255 255 / 0.5);
226
- }
227
- .live-dot {
228
- width: 6px;
229
- height: 6px;
230
- border-radius: 50%;
231
- background: currentColor;
232
- }
233
- .settings-anchor {
397
+
398
+ .fw-settings-anchor {
234
399
  position: relative;
235
400
  }
236
401
  `,
@@ -244,12 +409,21 @@ __decorate([
244
409
  __decorate([
245
410
  property({ type: Boolean, attribute: "is-content-live" })
246
411
  ], FwPlayerControls.prototype, "isContentLive", void 0);
412
+ __decorate([
413
+ property({ type: Boolean, attribute: "show-stats-button" })
414
+ ], FwPlayerControls.prototype, "showStatsButton", void 0);
247
415
  __decorate([
248
416
  property({ type: Boolean, attribute: "is-stats-open" })
249
417
  ], FwPlayerControls.prototype, "isStatsOpen", void 0);
250
418
  __decorate([
251
419
  state()
252
420
  ], FwPlayerControls.prototype, "_settingsOpen", void 0);
421
+ __decorate([
422
+ state()
423
+ ], FwPlayerControls.prototype, "_isNearLiveState", void 0);
424
+ __decorate([
425
+ state()
426
+ ], FwPlayerControls.prototype, "_buffered", void 0);
253
427
  FwPlayerControls = __decorate([
254
428
  customElement("fw-player-controls")
255
429
  ], FwPlayerControls);