@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
@@ -25,25 +25,248 @@ exports.FwPlayer = class FwPlayer extends lit.LitElement {
25
25
  this._isDevPanelOpen = false;
26
26
  this._skipDirection = null;
27
27
  this._contextMenuOpen = false;
28
+ this._contextMenuMounted = false;
29
+ this._contextMenuState = "closed";
30
+ this._contextMenuSide = "bottom";
31
+ this._contextMenuActiveLevel = "root";
32
+ this._contextMenuOpenSubmenu = null;
33
+ this._contextMenuSubmenuSide = "right";
28
34
  this._contextMenuX = 0;
29
35
  this._contextMenuY = 0;
30
36
  // ---- Controller ----
31
37
  this.pc = new playerControllerHost.PlayerControllerHost(this);
32
- // ---- Context Menu ----
38
+ this._contextMenuTypeahead = "";
39
+ this._resetContextMenuTypeahead = () => {
40
+ this._contextMenuTypeahead = "";
41
+ if (this._contextMenuTypeaheadTimer) {
42
+ clearTimeout(this._contextMenuTypeaheadTimer);
43
+ this._contextMenuTypeaheadTimer = undefined;
44
+ }
45
+ };
46
+ this._resolveContextMenuSide = (rawX, rawY, clampedX, clampedY) => {
47
+ const deltaX = Math.abs(rawX - clampedX);
48
+ const deltaY = Math.abs(rawY - clampedY);
49
+ if (deltaX === 0 && deltaY === 0) {
50
+ return "bottom";
51
+ }
52
+ if (deltaY >= deltaX) {
53
+ return rawY > clampedY ? "top" : "bottom";
54
+ }
55
+ return rawX > clampedX ? "left" : "right";
56
+ };
57
+ this._closeContextMenu = (restoreFocus = false) => {
58
+ this._contextMenuOpen = false;
59
+ this._contextMenuState = "closed";
60
+ this._contextMenuActiveLevel = "root";
61
+ this._contextMenuOpenSubmenu = null;
62
+ this._resetContextMenuTypeahead();
63
+ if (this._contextMenuCloseTimer) {
64
+ clearTimeout(this._contextMenuCloseTimer);
65
+ }
66
+ this._contextMenuCloseTimer = setTimeout(() => {
67
+ if (!this._contextMenuOpen) {
68
+ this._contextMenuMounted = false;
69
+ }
70
+ }, 170);
71
+ if (restoreFocus) {
72
+ const root = this.shadowRoot?.querySelector('[part="root"]');
73
+ root?.focus();
74
+ }
75
+ };
76
+ this._getQueryRoot = () => {
77
+ return (this.shadowRoot ?? this.renderRoot ?? null);
78
+ };
79
+ this._getContextMenuElement = () => this._getQueryRoot()?.querySelector('[data-context-menu="true"]') ?? null;
80
+ this._getContextMenuItems = (level = this._contextMenuActiveLevel) => Array.from(this._getQueryRoot()?.querySelectorAll(`[data-context-menu-item="true"][data-context-menu-level="${level}"]:not([data-disabled="true"])`) ?? []);
81
+ this._focusFirstContextMenuItem = (level = this._contextMenuActiveLevel) => {
82
+ const [firstItem] = this._getContextMenuItems(level);
83
+ firstItem?.focus();
84
+ };
85
+ this._focusPlaybackModeTrigger = () => {
86
+ const trigger = this._getQueryRoot()?.querySelector('[data-context-menu-trigger="playback-mode"]');
87
+ trigger?.focus();
88
+ };
89
+ this._openPlaybackModeSubmenu = () => {
90
+ this._contextMenuOpenSubmenu = "playback-mode";
91
+ this._contextMenuActiveLevel = "submenu";
92
+ this._resetContextMenuTypeahead();
93
+ queueMicrotask(() => {
94
+ this._syncPlaybackModeSubmenuSide();
95
+ this._focusFirstContextMenuItem("submenu");
96
+ });
97
+ };
98
+ this._closePlaybackModeSubmenu = (restoreTriggerFocus = false) => {
99
+ this._contextMenuOpenSubmenu = null;
100
+ this._contextMenuActiveLevel = "root";
101
+ this._resetContextMenuTypeahead();
102
+ if (restoreTriggerFocus) {
103
+ this._focusPlaybackModeTrigger();
104
+ }
105
+ };
106
+ this._clampContextMenuPosition = (x, y, width, height) => {
107
+ const viewportPadding = 8;
108
+ const maxX = Math.max(viewportPadding, window.innerWidth - width - viewportPadding);
109
+ const maxY = Math.max(viewportPadding, window.innerHeight - height - viewportPadding);
110
+ return {
111
+ x: Math.max(viewportPadding, Math.min(x, maxX)),
112
+ y: Math.max(viewportPadding, Math.min(y, maxY)),
113
+ };
114
+ };
115
+ this._syncContextMenuPosition = () => {
116
+ if (!this._contextMenuMounted)
117
+ return;
118
+ const menu = this._getContextMenuElement();
119
+ if (!menu)
120
+ return;
121
+ const rect = menu.getBoundingClientRect();
122
+ const next = this._clampContextMenuPosition(this._contextMenuX, this._contextMenuY, rect.width, rect.height);
123
+ this._contextMenuSide = this._resolveContextMenuSide(this._contextMenuX, this._contextMenuY, next.x, next.y);
124
+ if (next.x !== this._contextMenuX || next.y !== this._contextMenuY) {
125
+ this._contextMenuX = next.x;
126
+ this._contextMenuY = next.y;
127
+ }
128
+ };
129
+ this._syncPlaybackModeSubmenuSide = () => {
130
+ if (this._contextMenuOpenSubmenu !== "playback-mode")
131
+ return;
132
+ const menu = this._getContextMenuElement();
133
+ if (!menu)
134
+ return;
135
+ const rect = menu.getBoundingClientRect();
136
+ const estimatedSubmenuWidth = 190;
137
+ this._contextMenuSubmenuSide =
138
+ rect.right + estimatedSubmenuWidth > window.innerWidth - 8 ? "left" : "right";
139
+ };
140
+ this._openContextMenu = (x, y) => {
141
+ const next = this._clampContextMenuPosition(x, y, 220, 200);
142
+ this._contextMenuSide = this._resolveContextMenuSide(x, y, next.x, next.y);
143
+ this._contextMenuX = next.x;
144
+ this._contextMenuY = next.y;
145
+ this._contextMenuMounted = true;
146
+ this._contextMenuState = "open";
147
+ this._contextMenuActiveLevel = "root";
148
+ this._contextMenuOpenSubmenu = null;
149
+ if (this._contextMenuCloseTimer) {
150
+ clearTimeout(this._contextMenuCloseTimer);
151
+ this._contextMenuCloseTimer = undefined;
152
+ }
153
+ this._resetContextMenuTypeahead();
154
+ this._contextMenuOpen = true;
155
+ };
33
156
  this._handleContextMenu = (e) => {
157
+ const target = e.target;
158
+ if (target?.closest('[data-context-menu="true"]')) {
159
+ e.preventDefault();
160
+ return;
161
+ }
34
162
  e.preventDefault();
35
- this.getBoundingClientRect();
36
- this._contextMenuX = e.clientX;
37
- this._contextMenuY = e.clientY;
38
- this._contextMenuOpen = true;
163
+ this._openContextMenu(e.clientX, e.clientY);
164
+ };
165
+ this._handleContextMenuShortcut = (e) => {
166
+ const isContextMenuKey = e.key === "ContextMenu";
167
+ const isShiftF10 = e.key === "F10" && e.shiftKey;
168
+ if (!isContextMenuKey && !isShiftF10)
169
+ return;
170
+ e.preventDefault();
171
+ const rect = this.getBoundingClientRect();
172
+ const x = rect.left + rect.width / 2;
173
+ const y = rect.top + rect.height / 2;
174
+ this._openContextMenu(x, y);
39
175
  };
40
- this._handleDocumentClick = () => {
41
- if (this._contextMenuOpen)
42
- this._contextMenuOpen = false;
176
+ this._handleDocumentPointerDown = (e) => {
177
+ if (!this._contextMenuOpen)
178
+ return;
179
+ const menu = this._getContextMenuElement();
180
+ const composedPath = e.composedPath();
181
+ if (menu && composedPath.includes(menu))
182
+ return;
183
+ this._closeContextMenu();
43
184
  };
44
185
  this._handleDocumentContextMenu = (e) => {
186
+ if (!this._contextMenuOpen)
187
+ return;
45
188
  if (!this.contains(e.target)) {
46
- this._contextMenuOpen = false;
189
+ this._closeContextMenu();
190
+ }
191
+ };
192
+ this._handleDocumentKeyDown = (e) => {
193
+ if (e.key === "Escape" && this._contextMenuOpen) {
194
+ e.preventDefault();
195
+ this._closeContextMenu(true);
196
+ }
197
+ };
198
+ this._handleContextMenuKeyDown = (e) => {
199
+ if (!this._contextMenuOpen)
200
+ return;
201
+ const activeElement = this.shadowRoot?.activeElement;
202
+ if (e.key === "Escape") {
203
+ e.preventDefault();
204
+ if (this._contextMenuActiveLevel === "submenu") {
205
+ this._closePlaybackModeSubmenu(true);
206
+ }
207
+ else {
208
+ this._closeContextMenu(true);
209
+ }
210
+ return;
211
+ }
212
+ if (e.key === "Tab") {
213
+ this._closeContextMenu();
214
+ return;
215
+ }
216
+ if (e.key === "ArrowRight" && this._contextMenuActiveLevel === "root") {
217
+ if (activeElement?.dataset.contextMenuTrigger === "playback-mode") {
218
+ e.preventDefault();
219
+ this._openPlaybackModeSubmenu();
220
+ }
221
+ return;
222
+ }
223
+ if (e.key === "ArrowLeft" && this._contextMenuActiveLevel === "submenu") {
224
+ e.preventDefault();
225
+ this._closePlaybackModeSubmenu(true);
226
+ return;
227
+ }
228
+ const items = this._getContextMenuItems(this._contextMenuActiveLevel);
229
+ if (items.length === 0)
230
+ return;
231
+ const activeIndex = items.findIndex((item) => item === activeElement);
232
+ if (e.key === "Home") {
233
+ e.preventDefault();
234
+ this._focusFirstContextMenuItem(this._contextMenuActiveLevel);
235
+ return;
236
+ }
237
+ if (e.key === "End") {
238
+ e.preventDefault();
239
+ items[items.length - 1]?.focus();
240
+ return;
241
+ }
242
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
243
+ e.preventDefault();
244
+ const direction = e.key === "ArrowDown" ? 1 : -1;
245
+ const startIndex = activeIndex === -1 ? (direction === 1 ? 0 : items.length - 1) : activeIndex;
246
+ const nextIndex = (startIndex + direction + items.length) % items.length;
247
+ items[nextIndex]?.focus();
248
+ return;
249
+ }
250
+ if (e.key === "Enter" || e.key === " ") {
251
+ if (activeElement) {
252
+ e.preventDefault();
253
+ activeElement.click();
254
+ }
255
+ return;
256
+ }
257
+ if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) {
258
+ e.preventDefault();
259
+ this._contextMenuTypeahead += e.key.toLowerCase();
260
+ if (this._contextMenuTypeaheadTimer) {
261
+ clearTimeout(this._contextMenuTypeaheadTimer);
262
+ }
263
+ this._contextMenuTypeaheadTimer = setTimeout(() => {
264
+ this._resetContextMenuTypeahead();
265
+ }, 700);
266
+ const startIndex = activeIndex === -1 ? 0 : activeIndex + 1;
267
+ const orderedItems = [...items.slice(startIndex), ...items.slice(0, startIndex)];
268
+ const match = orderedItems.find((item) => item.textContent?.trim().toLowerCase().startsWith(this._contextMenuTypeahead));
269
+ match?.focus();
47
270
  }
48
271
  };
49
272
  }
@@ -78,19 +301,39 @@ exports.FwPlayer = class FwPlayer extends lit.LitElement {
78
301
  firstUpdated() {
79
302
  this.pc.attach(this._containerEl);
80
303
  // Close context menu on outside click
81
- document.addEventListener("pointerdown", this._handleDocumentClick);
304
+ document.addEventListener("pointerdown", this._handleDocumentPointerDown);
82
305
  document.addEventListener("contextmenu", this._handleDocumentContextMenu);
306
+ document.addEventListener("keydown", this._handleDocumentKeyDown);
83
307
  }
84
308
  disconnectedCallback() {
85
309
  super.disconnectedCallback();
86
- document.removeEventListener("pointerdown", this._handleDocumentClick);
310
+ document.removeEventListener("pointerdown", this._handleDocumentPointerDown);
87
311
  document.removeEventListener("contextmenu", this._handleDocumentContextMenu);
312
+ document.removeEventListener("keydown", this._handleDocumentKeyDown);
313
+ if (this._contextMenuCloseTimer) {
314
+ clearTimeout(this._contextMenuCloseTimer);
315
+ this._contextMenuCloseTimer = undefined;
316
+ }
317
+ this._resetContextMenuTypeahead();
88
318
  }
89
319
  updated(changed) {
90
320
  if (this.pc.s.toast) {
91
321
  clearTimeout(this._toastTimer);
92
322
  this._toastTimer = setTimeout(() => this.pc.dismissToast(), 3000);
93
323
  }
324
+ if ((changed.has("_contextMenuOpen") || changed.has("_contextMenuMounted")) &&
325
+ this._contextMenuOpen) {
326
+ queueMicrotask(() => {
327
+ this._syncContextMenuPosition();
328
+ this._focusFirstContextMenuItem("root");
329
+ });
330
+ }
331
+ if (changed.has("_contextMenuOpenSubmenu") &&
332
+ this._contextMenuOpenSubmenu === "playback-mode") {
333
+ queueMicrotask(() => {
334
+ this._syncPlaybackModeSubmenuSide();
335
+ });
336
+ }
94
337
  }
95
338
  // ---- Derived state ----
96
339
  get _showTitleOverlay() {
@@ -103,7 +346,14 @@ exports.FwPlayer = class FwPlayer extends lit.LitElement {
103
346
  }
104
347
  get _showWaitingForEndpoint() {
105
348
  const s = this.pc.s;
106
- return !s.endpoints && s.state !== "booting";
349
+ return !s.endpoints?.primary && s.state !== "booting";
350
+ }
351
+ get _waitingMessage() {
352
+ const s = this.pc.s;
353
+ if (this.gatewayUrl && s.state === "gateway_loading") {
354
+ return "Resolving viewing endpoint...";
355
+ }
356
+ return "Waiting for endpoint...";
107
357
  }
108
358
  get _useStockControls() {
109
359
  return this.controls || this.pc.s.currentPlayerInfo?.shortname === "mist-legacy";
@@ -174,11 +424,13 @@ exports.FwPlayer = class FwPlayer extends lit.LitElement {
174
424
  "overflow-hidden": true,
175
425
  flex: this.devMode,
176
426
  })}
427
+ data-player-container="true"
177
428
  tabindex="0"
178
429
  @mouseenter=${() => this.pc.handleMouseEnter()}
179
430
  @mouseleave=${() => this.pc.handleMouseLeave()}
180
431
  @mousemove=${() => this.pc.handleMouseMove()}
181
432
  @touchstart=${() => this.pc.handleTouchStart()}
433
+ @keydown=${this._handleContextMenuShortcut}
182
434
  @contextmenu=${this._handleContextMenu}
183
435
  >
184
436
  <!-- Player area -->
@@ -191,6 +443,16 @@ exports.FwPlayer = class FwPlayer extends lit.LitElement {
191
443
  <!-- Video container -->
192
444
  <div id="container" part="video-container" class="fw-player-container"></div>
193
445
 
446
+ <!-- Subtitle renderer -->
447
+ ${s.subtitlesEnabled
448
+ ? lit.html `
449
+ <fw-subtitle-renderer
450
+ .currentTime=${s.currentTime}
451
+ .enabled=${s.subtitlesEnabled}
452
+ ></fw-subtitle-renderer>
453
+ `
454
+ : lit.nothing}
455
+
194
456
  <!-- Title overlay -->
195
457
  ${this._showTitleOverlay
196
458
  ? lit.html `
@@ -230,7 +492,14 @@ exports.FwPlayer = class FwPlayer extends lit.LitElement {
230
492
  <!-- Waiting for endpoint -->
231
493
  ${this._showWaitingForEndpoint
232
494
  ? lit.html `
233
- <fw-idle-screen status="OFFLINE" message="Waiting for endpoint..."></fw-idle-screen>
495
+ <fw-idle-screen
496
+ status="OFFLINE"
497
+ .message=${this._waitingMessage}
498
+ @fw-retry=${() => {
499
+ this.pc.clearError();
500
+ this.pc.retry();
501
+ }}
502
+ ></fw-idle-screen>
234
503
  `
235
504
  : lit.nothing}
236
505
 
@@ -241,6 +510,10 @@ exports.FwPlayer = class FwPlayer extends lit.LitElement {
241
510
  .status=${s.isEffectivelyLive ? s.streamState?.status : undefined}
242
511
  .message=${s.isEffectivelyLive ? s.streamState?.message : "Loading video..."}
243
512
  .percentage=${s.isEffectivelyLive ? s.streamState?.percentage : undefined}
513
+ @fw-retry=${() => {
514
+ this.pc.clearError();
515
+ this.pc.retry();
516
+ }}
244
517
  ></fw-idle-screen>
245
518
  `
246
519
  : lit.nothing}
@@ -366,6 +639,9 @@ exports.FwPlayer = class FwPlayer extends lit.LitElement {
366
639
  .isStatsOpen=${this._isStatsOpen}
367
640
  @fw-stats-toggle=${() => {
368
641
  this._isStatsOpen = !this._isStatsOpen;
642
+ }}
643
+ @fw-mode-change=${(event) => {
644
+ this.playbackMode = event.detail.mode;
369
645
  }}
370
646
  ></fw-player-controls>
371
647
  `
@@ -380,6 +656,9 @@ exports.FwPlayer = class FwPlayer extends lit.LitElement {
380
656
  .playbackMode=${this.playbackMode}
381
657
  @fw-close=${() => {
382
658
  this._isDevPanelOpen = false;
659
+ }}
660
+ @fw-playback-mode-change=${(event) => {
661
+ this.playbackMode = event.detail.mode;
383
662
  }}
384
663
  ></fw-dev-mode-panel>
385
664
  `
@@ -387,17 +666,31 @@ exports.FwPlayer = class FwPlayer extends lit.LitElement {
387
666
  </div>
388
667
 
389
668
  <!-- Context menu -->
390
- ${this._contextMenuOpen
669
+ <!-- Keep menu in-shadow (no document portal) to preserve host-scoped styling and avoid a global overlay manager. -->
670
+ ${this._contextMenuMounted
391
671
  ? lit.html `
392
672
  <div
393
- class="context-menu"
394
- style="left: ${this._contextMenuX}px; top: ${this._contextMenuY}px;"
673
+ data-context-menu="true"
674
+ data-state=${this._contextMenuState}
675
+ data-side=${this._contextMenuSide}
676
+ class="fw-player-surface fw-context-menu"
677
+ role="menu"
678
+ aria-label="Player options"
679
+ tabindex="-1"
680
+ style="position: fixed; left: ${this._contextMenuX}px; top: ${this._contextMenuY}px;"
681
+ @contextmenu=${(e) => e.preventDefault()}
682
+ @keydown=${this._handleContextMenuKeyDown}
395
683
  >
396
684
  <button
397
- class="context-menu-item"
685
+ type="button"
686
+ role="menuitem"
687
+ tabindex="-1"
688
+ data-context-menu-item="true"
689
+ data-context-menu-level="root"
690
+ class="fw-context-menu-item gap-2"
398
691
  @click=${() => {
399
692
  this._isStatsOpen = !this._isStatsOpen;
400
- this._contextMenuOpen = false;
693
+ this._closeContextMenu();
401
694
  }}
402
695
  >
403
696
  <span class="opacity-70 shrink-0">${index.statsIcon(14)}</span>
@@ -405,12 +698,17 @@ exports.FwPlayer = class FwPlayer extends lit.LitElement {
405
698
  </button>
406
699
  ${this.devMode
407
700
  ? lit.html `
408
- <div class="context-menu-sep"></div>
701
+ <div class="fw-context-menu-separator"></div>
409
702
  <button
410
- class="context-menu-item"
703
+ type="button"
704
+ role="menuitem"
705
+ tabindex="-1"
706
+ data-context-menu-item="true"
707
+ data-context-menu-level="root"
708
+ class="fw-context-menu-item gap-2"
411
709
  @click=${() => {
412
710
  this._isDevPanelOpen = !this._isDevPanelOpen;
413
- this._contextMenuOpen = false;
711
+ this._closeContextMenu();
414
712
  }}
415
713
  >
416
714
  <span class="opacity-70 shrink-0">${index.settingsIcon(14)}</span>
@@ -418,27 +716,145 @@ exports.FwPlayer = class FwPlayer extends lit.LitElement {
418
716
  </button>
419
717
  `
420
718
  : lit.nothing}
421
- <div class="context-menu-sep"></div>
719
+ <div class="fw-context-menu-separator"></div>
422
720
  <button
423
- class="context-menu-item"
721
+ type="button"
722
+ role="menuitemcheckbox"
723
+ aria-checked=${String(s.isPiPActive)}
724
+ tabindex="-1"
725
+ data-context-menu-item="true"
726
+ data-context-menu-level="root"
727
+ class="fw-context-menu-item gap-2"
424
728
  @click=${() => {
425
729
  this.pc.togglePiP();
426
- this._contextMenuOpen = false;
730
+ this._closeContextMenu();
427
731
  }}
428
732
  >
429
733
  <span class="opacity-70 shrink-0">${index.pictureInPictureIcon(14)}</span>
430
734
  <span>Picture-in-Picture</span>
431
735
  </button>
432
736
  <button
433
- class="context-menu-item"
737
+ type="button"
738
+ role="menuitemcheckbox"
739
+ aria-checked=${String(s.isLoopEnabled)}
740
+ tabindex="-1"
741
+ data-context-menu-item="true"
742
+ data-context-menu-level="root"
743
+ class="fw-context-menu-item gap-2"
434
744
  @click=${() => {
435
745
  this.pc.toggleLoop();
436
- this._contextMenuOpen = false;
746
+ this._closeContextMenu();
437
747
  }}
438
748
  >
439
749
  <span class="opacity-70 shrink-0">${index.loopIcon(14)}</span>
440
750
  <span>${s.isLoopEnabled ? "Disable Loop" : "Enable Loop"}</span>
441
751
  </button>
752
+ ${this.devMode
753
+ ? lit.html `
754
+ <div class="fw-context-menu-separator"></div>
755
+ <button
756
+ type="button"
757
+ role="menuitem"
758
+ aria-haspopup="menu"
759
+ aria-expanded=${String(this._contextMenuOpenSubmenu === "playback-mode")}
760
+ tabindex="-1"
761
+ data-context-menu-item="true"
762
+ data-context-menu-level="root"
763
+ data-context-menu-trigger="playback-mode"
764
+ class="fw-context-menu-item gap-2"
765
+ @click=${() => {
766
+ if (this._contextMenuOpenSubmenu === "playback-mode") {
767
+ this._closePlaybackModeSubmenu();
768
+ }
769
+ else {
770
+ this._openPlaybackModeSubmenu();
771
+ }
772
+ }}
773
+ >
774
+ <span>Playback Mode</span>
775
+ <span class="ml-auto opacity-70">›</span>
776
+ </button>
777
+ ${this._contextMenuOpenSubmenu === "playback-mode"
778
+ ? lit.html `
779
+ <div
780
+ class="fw-player-surface fw-context-menu"
781
+ role="menu"
782
+ aria-label="Playback mode options"
783
+ data-state="open"
784
+ data-side=${this._contextMenuSubmenuSide === "right" ? "right" : "left"}
785
+ style=${this._contextMenuSubmenuSide === "right"
786
+ ? "position:absolute; top:0; left: calc(100% + 4px);"
787
+ : "position:absolute; top:0; right: calc(100% + 4px);"}
788
+ >
789
+ <button
790
+ type="button"
791
+ role="menuitemradio"
792
+ aria-checked=${String(this.playbackMode === "auto")}
793
+ tabindex="-1"
794
+ data-context-menu-item="true"
795
+ data-context-menu-level="submenu"
796
+ class="fw-context-menu-item gap-2"
797
+ @click=${() => {
798
+ this.playbackMode = "auto";
799
+ void this.pc.setDevModeOptions({ playbackMode: "auto" });
800
+ this._closeContextMenu();
801
+ }}
802
+ >
803
+ <span>Auto</span>
804
+ </button>
805
+ <button
806
+ type="button"
807
+ role="menuitemradio"
808
+ aria-checked=${String(this.playbackMode === "low-latency")}
809
+ tabindex="-1"
810
+ data-context-menu-item="true"
811
+ data-context-menu-level="submenu"
812
+ class="fw-context-menu-item gap-2"
813
+ @click=${() => {
814
+ this.playbackMode = "low-latency";
815
+ void this.pc.setDevModeOptions({ playbackMode: "low-latency" });
816
+ this._closeContextMenu();
817
+ }}
818
+ >
819
+ <span>Low Latency</span>
820
+ </button>
821
+ <button
822
+ type="button"
823
+ role="menuitemradio"
824
+ aria-checked=${String(this.playbackMode === "quality")}
825
+ tabindex="-1"
826
+ data-context-menu-item="true"
827
+ data-context-menu-level="submenu"
828
+ class="fw-context-menu-item gap-2"
829
+ @click=${() => {
830
+ this.playbackMode = "quality";
831
+ void this.pc.setDevModeOptions({ playbackMode: "quality" });
832
+ this._closeContextMenu();
833
+ }}
834
+ >
835
+ <span>Quality</span>
836
+ </button>
837
+ <button
838
+ type="button"
839
+ role="menuitemradio"
840
+ aria-checked=${String(this.playbackMode === "vod")}
841
+ tabindex="-1"
842
+ data-context-menu-item="true"
843
+ data-context-menu-level="submenu"
844
+ class="fw-context-menu-item gap-2"
845
+ @click=${() => {
846
+ this.playbackMode = "vod";
847
+ void this.pc.setDevModeOptions({ playbackMode: "vod" });
848
+ this._closeContextMenu();
849
+ }}
850
+ >
851
+ <span>VOD</span>
852
+ </button>
853
+ </div>
854
+ `
855
+ : lit.nothing}
856
+ `
857
+ : lit.nothing}
442
858
  </div>
443
859
  `
444
860
  : lit.nothing}
@@ -468,40 +884,6 @@ exports.FwPlayer.styles = [
468
884
  flex: 1;
469
885
  min-width: 0;
470
886
  }
471
- .context-menu {
472
- position: fixed;
473
- z-index: 200;
474
- min-width: 180px;
475
- border-radius: 0.5rem;
476
- border: 1px solid rgb(255 255 255 / 0.1);
477
- background: rgb(0 0 0 / 0.9);
478
- backdrop-filter: blur(8px);
479
- padding: 0.25rem;
480
- box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.3);
481
- }
482
- .context-menu-item {
483
- display: flex;
484
- align-items: center;
485
- gap: 0.5rem;
486
- width: 100%;
487
- padding: 0.375rem 0.5rem;
488
- border: none;
489
- background: none;
490
- color: rgb(255 255 255 / 0.8);
491
- font-size: 0.8125rem;
492
- cursor: pointer;
493
- border-radius: 0.25rem;
494
- text-align: left;
495
- }
496
- .context-menu-item:hover {
497
- background: rgb(255 255 255 / 0.1);
498
- color: white;
499
- }
500
- .context-menu-sep {
501
- height: 1px;
502
- background: rgb(255 255 255 / 0.1);
503
- margin: 0.25rem 0;
504
- }
505
887
  `,
506
888
  ];
507
889
  tslib_es6.__decorate([
@@ -555,6 +937,24 @@ tslib_es6.__decorate([
555
937
  tslib_es6.__decorate([
556
938
  decorators_js.state()
557
939
  ], exports.FwPlayer.prototype, "_contextMenuOpen", void 0);
940
+ tslib_es6.__decorate([
941
+ decorators_js.state()
942
+ ], exports.FwPlayer.prototype, "_contextMenuMounted", void 0);
943
+ tslib_es6.__decorate([
944
+ decorators_js.state()
945
+ ], exports.FwPlayer.prototype, "_contextMenuState", void 0);
946
+ tslib_es6.__decorate([
947
+ decorators_js.state()
948
+ ], exports.FwPlayer.prototype, "_contextMenuSide", void 0);
949
+ tslib_es6.__decorate([
950
+ decorators_js.state()
951
+ ], exports.FwPlayer.prototype, "_contextMenuActiveLevel", void 0);
952
+ tslib_es6.__decorate([
953
+ decorators_js.state()
954
+ ], exports.FwPlayer.prototype, "_contextMenuOpenSubmenu", void 0);
955
+ tslib_es6.__decorate([
956
+ decorators_js.state()
957
+ ], exports.FwPlayer.prototype, "_contextMenuSubmenuSide", void 0);
558
958
  tslib_es6.__decorate([
559
959
  decorators_js.state()
560
960
  ], exports.FwPlayer.prototype, "_contextMenuX", void 0);