Skip to content

Commit be96bd2

Browse files
committed
fix(player): stop expensive updates when not visible
1 parent c0b30d7 commit be96bd2

20 files changed

Lines changed: 345 additions & 153 deletions

File tree

packages/react/src/components/layouts/default/ui/menus/chapters-menu.tsx

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -31,51 +31,67 @@ function DefaultChaptersMenu({ tooltip, placement, portalClass }: DefaultMediaMe
3131
$src = useMediaState('currentSrc'),
3232
$viewType = useMediaState('viewType'),
3333
$offset = !isSmallLayout && menuGroup === 'bottom' && $viewType === 'video' ? 26 : 0,
34-
$RemotionThumbnail = useSignal(RemotionThumbnail);
34+
$RemotionThumbnail = useSignal(RemotionThumbnail),
35+
[isOpen, setIsOpen] = React.useState(false);
3536

3637
if (disabled) return null;
3738

39+
function onOpen() {
40+
setIsOpen(true);
41+
}
42+
43+
function onClose() {
44+
setIsOpen(false);
45+
}
46+
3847
const Content = (
3948
<Menu.Content
4049
className="vds-chapters-menu-items vds-menu-items"
4150
placement={placement}
4251
offset={$offset}
4352
>
44-
<Menu.RadioGroup
45-
className="vds-chapters-radio-group vds-radio-group"
46-
value={options.selectedValue}
47-
data-thumbnails={thumbnails ? '' : null}
48-
>
49-
{options.map(
50-
({ cue, label, value, startTimeText, durationText, select, setProgressVar }) => (
51-
<Menu.Radio
52-
className="vds-chapter-radio vds-radio"
53-
value={value}
54-
key={value}
55-
onSelect={select}
56-
ref={setProgressVar}
57-
>
58-
{thumbnails ? (
59-
<Thumbnail.Root src={thumbnails} className="vds-thumbnail" time={cue.startTime}>
60-
<Thumbnail.Img />
61-
</Thumbnail.Root>
62-
) : $RemotionThumbnail && isRemotionSource($src) ? (
63-
<$RemotionThumbnail className="vds-thumbnail" frame={cue.startTime * $src.fps!} />
64-
) : null}
65-
<div className="vds-chapter-radio-content">
66-
<span className="vds-chapter-radio-label">{label}</span>
67-
<span className="vds-chapter-radio-start-time">{startTimeText}</span>
68-
<span className="vds-chapter-radio-duration">{durationText}</span>
69-
</div>
70-
</Menu.Radio>
71-
),
72-
)}
73-
</Menu.RadioGroup>
53+
{isOpen ? (
54+
<Menu.RadioGroup
55+
className="vds-chapters-radio-group vds-radio-group"
56+
value={options.selectedValue}
57+
data-thumbnails={thumbnails ? '' : null}
58+
>
59+
{options.map(
60+
({ cue, label, value, startTimeText, durationText, select, setProgressVar }) => (
61+
<Menu.Radio
62+
className="vds-chapter-radio vds-radio"
63+
value={value}
64+
key={value}
65+
onSelect={select}
66+
ref={setProgressVar}
67+
>
68+
{thumbnails ? (
69+
<Thumbnail.Root src={thumbnails} className="vds-thumbnail" time={cue.startTime}>
70+
<Thumbnail.Img />
71+
</Thumbnail.Root>
72+
) : $RemotionThumbnail && isRemotionSource($src) ? (
73+
<$RemotionThumbnail className="vds-thumbnail" frame={cue.startTime * $src.fps!} />
74+
) : null}
75+
<div className="vds-chapter-radio-content">
76+
<span className="vds-chapter-radio-label">{label}</span>
77+
<span className="vds-chapter-radio-start-time">{startTimeText}</span>
78+
<span className="vds-chapter-radio-duration">{durationText}</span>
79+
</div>
80+
</Menu.Radio>
81+
),
82+
)}
83+
</Menu.RadioGroup>
84+
) : null}
7485
</Menu.Content>
7586
);
7687

7788
return (
78-
<Menu.Root className="vds-chapters-menu vds-menu" showDelay={showMenuDelay}>
89+
<Menu.Root
90+
className="vds-chapters-menu vds-menu"
91+
showDelay={showMenuDelay}
92+
onOpen={onOpen}
93+
onClose={onClose}
94+
>
7995
<DefaultTooltip content={chaptersText} placement={tooltip}>
8096
<Menu.Button
8197
className="vds-menu-button vds-button"

packages/react/src/components/layouts/default/ui/menus/settings-menu.tsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,25 +29,43 @@ function DefaultSettingsMenu({ tooltip, placement, portalClass, slots }: Default
2929
} = useDefaultLayoutContext(),
3030
settingsText = useDefaultLayoutWord('Settings'),
3131
$viewType = useMediaState('viewType'),
32-
$offset = !isSmallLayout && menuGroup === 'bottom' && $viewType === 'video' ? 26 : 0;
32+
$offset = !isSmallLayout && menuGroup === 'bottom' && $viewType === 'video' ? 26 : 0,
33+
[isOpen, setIsOpen] = React.useState(false);
34+
35+
function onOpen() {
36+
setIsOpen(true);
37+
}
38+
39+
function onClose() {
40+
setIsOpen(false);
41+
}
3342

3443
const Content = (
3544
<Menu.Content
3645
className="vds-settings-menu-items vds-menu-items"
3746
placement={placement}
3847
offset={$offset}
3948
>
40-
{slot(slots, 'settingsMenuStartItems', null)}
41-
<DefaultPlaybackMenu />
42-
<DefaultAccessibilityMenu />
43-
<DefaultAudioMenu />
44-
<DefaultCaptionMenu />
45-
{slot(slots, 'settingsMenuEndItems', null)}
49+
{isOpen ? (
50+
<>
51+
{slot(slots, 'settingsMenuStartItems', null)}
52+
<DefaultPlaybackMenu />
53+
<DefaultAccessibilityMenu />
54+
<DefaultAudioMenu />
55+
<DefaultCaptionMenu />
56+
{slot(slots, 'settingsMenuEndItems', null)}
57+
</>
58+
) : null}
4659
</Menu.Content>
4760
);
4861

4962
return (
50-
<Menu.Root className="vds-settings-menu vds-menu" showDelay={showMenuDelay}>
63+
<Menu.Root
64+
className="vds-settings-menu vds-menu"
65+
showDelay={showMenuDelay}
66+
onOpen={onOpen}
67+
onClose={onClose}
68+
>
5169
<DefaultTooltip content={settingsText} placement={tooltip}>
5270
<Menu.Button className="vds-menu-button vds-button" aria-label={settingsText}>
5371
<Icons.Menu.Settings className="vds-icon vds-rotate-icon" />

packages/vidstack/mangle.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -822,5 +822,15 @@
822822
"_submenu": "no",
823823
"_watchChapterTitle": "po",
824824
"_watchColorScheme": "qo",
825-
"_watchStep": "ro"
825+
"_watchStep": "ro",
826+
"_onSourceQualitiesChange": "vo",
827+
"_stopQualityResizeEffect": "to",
828+
"_stopWatchingQualityResize": "so",
829+
"_watchQualityResize": "uo",
830+
"_watchSource": "wo",
831+
"_focusActive": "Ao",
832+
"_isSubmenuOpen": "zo",
833+
"_isTransitionActive": "yo",
834+
"_updateFocus": "Bo",
835+
"_wasKeyboardExpand": "xo"
826836
}

packages/vidstack/src/components/layouts/plyr/plyr-layout.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,22 @@ export class PlyrLayout extends Component<PlyrLayoutProps> {
2222

2323
export function usePlyrLayoutClasses(el: HTMLElement, media: MediaContext) {
2424
const {
25-
fullscreen,
25+
canAirPlay,
2626
canFullscreen,
2727
canPictureInPicture,
28-
pictureInPicture,
28+
controlsHidden,
29+
currentTime,
30+
fullscreen,
2931
hasCaptions,
30-
textTrack,
31-
canAirPlay,
3232
isAirPlayConnected,
33-
viewType,
34-
playing,
3533
paused,
36-
controlsVisible,
34+
pictureInPicture,
35+
playing,
3736
pointer,
38-
waiting,
39-
currentTime,
4037
poster,
38+
textTrack,
39+
viewType,
40+
waiting,
4141
} = media.$state;
4242

4343
el.classList.add('plyr');
@@ -48,7 +48,7 @@ export function usePlyrLayoutClasses(el: HTMLElement, media: MediaContext) {
4848
'plyr--airplay-supported': canAirPlay,
4949
'plyr--fullscreen-active': fullscreen,
5050
'plyr--fullscreen-enabled': canFullscreen,
51-
'plyr--hide-controls': () => !controlsVisible(),
51+
'plyr--hide-controls': controlsHidden,
5252
'plyr--is-touch': () => pointer() === 'coarse',
5353
'plyr--loading': waiting,
5454
'plyr--paused': paused,

packages/vidstack/src/components/ui/menu/menu-focus-controller.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ export class MenuFocusController {
4444

4545
_attachMenu(el: HTMLElement) {
4646
listenEvent(el, 'focus', this._onFocus.bind(this));
47+
4748
this._el = el;
4849
onDispose(() => {
4950
this._el = null;
5051
});
51-
return this;
5252
}
5353

5454
_listen() {
@@ -81,23 +81,30 @@ export class MenuFocusController {
8181
}
8282
}
8383

84+
_focusActive() {
85+
const index = this._findActiveIndex();
86+
this._focusAt(index >= 0 ? index : 0);
87+
}
88+
8489
protected _focusAt(index: number) {
8590
this._index = index;
86-
this._elements[index]?.focus();
87-
this._scroll(index);
91+
if (this._elements[index]) {
92+
this._elements[index].focus();
93+
this._scroll(index);
94+
} else {
95+
this._el?.focus();
96+
}
8897
}
8998

9099
protected _findActiveIndex() {
91-
return this._elements.findIndex((el) => el.getAttribute('aria-checked') === 'true');
100+
return this._elements.findIndex(
101+
(el) => document.activeElement === el || el.getAttribute('aria-checked') === 'true',
102+
);
92103
}
93104

94105
protected _onFocus() {
95106
this._update();
96-
// Timeout to allow size to be updated via transition.
97-
setTimeout(() => {
98-
const index = this._findActiveIndex();
99-
this._focusAt(index >= 0 ? index : 0);
100-
}, 100);
107+
this._focusActive();
101108
}
102109

103110
protected _onKeyUp(event: KeyboardEvent) {

packages/vidstack/src/components/ui/menu/menu.ts

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,6 @@ export class Menu extends Component<MenuProps, {}, MenuEvents> {
166166

167167
protected override onAttach(el: HTMLElement) {
168168
el.style.setProperty('display', 'contents');
169-
this._focus._attachMenu(el);
170169
}
171170

172171
protected override onConnect(el: HTMLElement) {
@@ -294,7 +293,10 @@ export class Menu extends Component<MenuProps, {}, MenuEvents> {
294293
this._isTriggerDisabled.set(disabled);
295294
}
296295

296+
private _wasKeyboardExpand = false;
297297
private _onExpandedChange(isExpanded: boolean, event?: Event) {
298+
this._wasKeyboardExpand = isKeyboardEvent(event);
299+
298300
event?.stopPropagation();
299301

300302
if (this._expanded() === isExpanded) return;
@@ -325,12 +327,9 @@ export class Menu extends Component<MenuProps, {}, MenuEvents> {
325327
this._toggleMediaControls(event);
326328
tick();
327329

328-
if (isKeyboardEvent(event)) {
329-
if (isExpanded) {
330-
content?.focus();
331-
} else {
332-
trigger?.focus();
333-
}
330+
if (this._wasKeyboardExpand) {
331+
if (isExpanded) content?.focus();
332+
else trigger?.focus();
334333

335334
for (const el of [this.el, content]) {
336335
el && el.setAttribute('data-keyboard', '');
@@ -352,28 +351,26 @@ export class Menu extends Component<MenuProps, {}, MenuEvents> {
352351
this._menuObserver?._onOpen?.(event);
353352
} else {
354353
if (this.isSubmenu) {
355-
// A little delay so submenu closing doesn't jump menu size when closing.
356-
setTimeout(() => {
357-
for (const el of this._submenus) el.close(event);
358-
}, 300);
354+
for (const el of this._submenus) el.close(event);
359355
} else {
360356
this._media.activeMenu = null;
361357
}
362358

363359
this._menuObserver?._onClose?.(event);
364360
}
365361

366-
if (isExpanded && !isKeyboardEvent(event)) {
367-
requestAnimationFrame(() => {
368-
this._focus._update();
369-
// Timeout to allow size to be updated via transition.
370-
setTimeout(() => {
371-
this._focus._scroll();
372-
}, 100);
373-
});
362+
if (isExpanded) {
363+
requestAnimationFrame(this._updateFocus.bind(this));
374364
}
375365
}
376366

367+
private _updateFocus() {
368+
if (this._isTransitionActive || this._isSubmenuOpen) return;
369+
this._focus._update();
370+
if (this._wasKeyboardExpand) this._focus._focusActive();
371+
this._focus._scroll();
372+
}
373+
377374
private _isExpanded() {
378375
return !this._isDisabled() && this._expanded();
379376
}
@@ -404,9 +401,7 @@ export class Menu extends Component<MenuProps, {}, MenuEvents> {
404401
return;
405402
}
406403

407-
// A little delay so submenu closing doesn't jump menu size when closing.
408-
if (this.isSubmenu) return setTimeout(this.close.bind(this, event), 800);
409-
else this.close(event);
404+
this.close(event);
410405
}
411406

412407
private _getCloseTarget() {
@@ -453,8 +448,11 @@ export class Menu extends Component<MenuProps, {}, MenuEvents> {
453448
this._submenus.delete(menu);
454449
}
455450

451+
private _isSubmenuOpen = false;
456452
private _onSubmenuOpenBind = this._onSubmenuOpen.bind(this);
457453
private _onSubmenuOpen(event: MenuOpenEvent) {
454+
this._isSubmenuOpen = true;
455+
458456
const content = this._content();
459457

460458
if (this.isSubmenu) {
@@ -483,6 +481,8 @@ export class Menu extends Component<MenuProps, {}, MenuEvents> {
483481

484482
private _onSubmenuCloseBind = this._onSubmenuClose.bind(this);
485483
private _onSubmenuClose(event: MenuCloseEvent) {
484+
this._isSubmenuOpen = false;
485+
486486
const content = this._content();
487487

488488
if (this.isSubmenu) {
@@ -533,11 +533,15 @@ export class Menu extends Component<MenuProps, {}, MenuEvents> {
533533
setStyle(content, '--menu-height', height + 'px');
534534
});
535535

536+
protected _isTransitionActive = false;
536537
protected _onResizeTransition(event: TransitionEvent) {
537538
const content = this._content();
538539
if (content && event.propertyName === 'height') {
539-
const hasStarted = event.type === 'transitionstart';
540-
setAttribute(content, 'data-resizing', hasStarted);
540+
this._isTransitionActive = event.type === 'transitionstart';
541+
542+
setAttribute(content, 'data-resizing', this._isTransitionActive);
543+
544+
if (this._expanded()) this._updateFocus();
541545
}
542546
}
543547

0 commit comments

Comments
 (0)