fix(skin): improve buffering, overlays, and input feedback (#1547)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Sam Potts
2026-07-01 09:49:12 +10:00
committed by GitHub
co-authored by Cursor
parent 7c3be22e8a
commit 0de3fef878
55 changed files with 864 additions and 544 deletions
@@ -9,6 +9,7 @@ import {
iconFlipped,
iconState,
menu,
playButton,
playbackRate,
popup,
root,
@@ -48,15 +49,20 @@ function getTemplateHTML() {
<div class="${controls}">
<media-tooltip-group>
<div class="${buttonGroup}">
<media-play-button commandfor="play-tooltip" class="${cn(button.base, button.subtle, button.icon, iconState.play.button)}">
<span class="${playButton.wrapper}">
<media-buffering-indicator class="${playButton.bufferingRoot}">
${renderIcon('spinner', { class: icon })}
</media-buffering-indicator>
<media-play-button commandfor="play-tooltip" class="${cn(button.base, button.subtle, button.icon, iconState.play.button, playButton.control)}">
${renderIcon('restart', { class: cn(icon, iconState.play.restart) })}
${renderIcon('play', { class: cn(icon, iconState.play.play) })}
${renderIcon('pause', { class: cn(icon, iconState.play.pause) })}
</media-play-button>
<media-tooltip id="play-tooltip" side="top" boundary="viewport" class="${cn(popup.tooltip)}">
<media-tooltip-label></media-tooltip-label>
<media-tooltip-shortcut class="${popup.tooltipShortcut}"></media-tooltip-shortcut>
</media-tooltip>
<media-tooltip-label></media-tooltip-label>
<media-tooltip-shortcut class="${popup.tooltipShortcut}"></media-tooltip-shortcut>
</media-tooltip>
</span>
<media-seek-button commandfor="seek-backward-tooltip" seconds="${-SEEK_TIME}" class="${cn(button.base, button.subtle, button.icon)}">
<span class="${iconContainer}">
+14 -9
View File
@@ -31,15 +31,20 @@ function getTemplateHTML() {
<div class="media-controls">
<media-tooltip-group>
<div class="media-button-group">
<media-play-button commandfor="play-tooltip" class="media-button media-button--subtle media-button--icon media-button--play">
${renderIcon('restart', { class: 'media-icon media-icon--restart' })}
${renderIcon('play', { class: 'media-icon media-icon--play' })}
${renderIcon('pause', { class: 'media-icon media-icon--pause' })}
</media-play-button>
<media-tooltip id="play-tooltip" side="top" boundary="viewport" class="media-tooltip">
<media-tooltip-label></media-tooltip-label>
<media-tooltip-shortcut class="media-tooltip__kbd"></media-tooltip-shortcut>
</media-tooltip>
<span class="media-button--play__wrapper">
<media-buffering-indicator class="media-buffering-indicator">
${renderIcon('spinner', { class: 'media-icon' })}
</media-buffering-indicator>
<media-play-button commandfor="play-tooltip" class="media-button media-button--subtle media-button--icon media-button--play">
${renderIcon('restart', { class: 'media-icon media-icon--restart' })}
${renderIcon('play', { class: 'media-icon media-icon--play' })}
${renderIcon('pause', { class: 'media-icon media-icon--pause' })}
</media-play-button>
<media-tooltip id="play-tooltip" side="top" boundary="viewport" class="media-tooltip">
<media-tooltip-label></media-tooltip-label>
<media-tooltip-shortcut class="media-tooltip__kbd"></media-tooltip-shortcut>
</media-tooltip>
</span>
<media-seek-button commandfor="seek-backward-tooltip" seconds="${-SEEK_TIME}" class="media-button media-button--subtle media-button--icon media-button--seek">
<span class="media-icon__container">
@@ -2,6 +2,7 @@
// used by the minimal skin without creating a skin element. Use this entry
// when building an ejected (light DOM) player layout.
import { MediaContainerElement } from '../../media/container-element';
import { BufferingIndicatorElement } from '../../ui/buffering-indicator/buffering-indicator-element';
import { HotkeyElement } from '../../ui/hotkey/hotkey-element';
import { MuteButtonElement } from '../../ui/mute-button/mute-button-element';
import { PlayButtonElement } from '../../ui/play-button/play-button-element';
@@ -32,6 +33,7 @@ defineTime();
defineMenu();
// Standalone elements.
safeDefine(BufferingIndicatorElement);
safeDefine(HotkeyElement);
safeDefine(MuteButtonElement);
safeDefine(PlayButtonElement);
@@ -9,6 +9,7 @@ import {
iconFlipped,
iconState,
menu,
playButton,
playbackRate,
popup,
root,
@@ -48,15 +49,20 @@ function getTemplateHTML() {
<div class="${controls}">
<media-tooltip-group>
<div class="${buttonGroup}">
<media-play-button commandfor="play-tooltip" class="${cn(button.base, button.subtle, button.icon, iconState.play.button)}">
<span class="${playButton.wrapper}">
<media-buffering-indicator class="${playButton.bufferingRoot}">
${renderIcon('spinner', { class: icon })}
</media-buffering-indicator>
<media-play-button commandfor="play-tooltip" class="${cn(button.base, button.subtle, button.icon, iconState.play.button, playButton.control)}">
${renderIcon('restart', { class: cn(icon, iconState.play.restart) })}
${renderIcon('play', { class: cn(icon, iconState.play.play) })}
${renderIcon('pause', { class: cn(icon, iconState.play.pause) })}
</media-play-button>
<media-tooltip id="play-tooltip" side="top" boundary="viewport" class="${cn(popup.tooltip)}">
<media-tooltip-label></media-tooltip-label>
<media-tooltip-shortcut class="${popup.tooltipShortcut}"></media-tooltip-shortcut>
</media-tooltip>
<media-tooltip-label></media-tooltip-label>
<media-tooltip-shortcut class="${popup.tooltipShortcut}"></media-tooltip-shortcut>
</media-tooltip>
</span>
<media-seek-button commandfor="seek-backward-tooltip" seconds="${-SEEK_TIME}" class="${cn(button.base, button.subtle, button.icon)}">
<span class="${iconContainer}">
+14 -9
View File
@@ -31,15 +31,20 @@ function getTemplateHTML() {
<div class="media-surface media-controls">
<media-tooltip-group>
<div class="media-button-group">
<media-play-button commandfor="play-tooltip" class="media-button media-button--subtle media-button--icon media-button--play">
${renderIcon('restart', { class: 'media-icon media-icon--restart' })}
${renderIcon('play', { class: 'media-icon media-icon--play' })}
${renderIcon('pause', { class: 'media-icon media-icon--pause' })}
</media-play-button>
<media-tooltip id="play-tooltip" side="top" boundary="viewport" class="media-surface media-tooltip">
<media-tooltip-label></media-tooltip-label>
<media-tooltip-shortcut class="media-tooltip__kbd"></media-tooltip-shortcut>
</media-tooltip>
<span class="media-button--play__wrapper">
<media-buffering-indicator class="media-buffering-indicator">
${renderIcon('spinner', { class: 'media-icon' })}
</media-buffering-indicator>
<media-play-button commandfor="play-tooltip" class="media-button media-button--subtle media-button--icon media-button--play">
${renderIcon('restart', { class: 'media-icon media-icon--restart' })}
${renderIcon('play', { class: 'media-icon media-icon--play' })}
${renderIcon('pause', { class: 'media-icon media-icon--pause' })}
</media-play-button>
<media-tooltip id="play-tooltip" side="top" boundary="viewport" class="media-surface media-tooltip">
<media-tooltip-label></media-tooltip-label>
<media-tooltip-shortcut class="media-tooltip__kbd"></media-tooltip-shortcut>
</media-tooltip>
</span>
<media-seek-button commandfor="seek-backward-tooltip" seconds="${-SEEK_TIME}" class="media-button media-button--subtle media-button--icon media-button--seek">
<span class="media-icon__container">
+2
View File
@@ -2,6 +2,7 @@
// without creating a skin element. Use this entry when building an ejected
// (light DOM) player layout.
import { MediaContainerElement } from '../../media/container-element';
import { BufferingIndicatorElement } from '../../ui/buffering-indicator/buffering-indicator-element';
import { GestureElement } from '../../ui/gesture/gesture-element';
import { HotkeyElement } from '../../ui/hotkey/hotkey-element';
import { LiveButtonElement } from '../../ui/live-button/live-button-element';
@@ -35,6 +36,7 @@ defineMenu();
// Standalone elements.
safeDefine(GestureElement);
safeDefine(HotkeyElement);
safeDefine(BufferingIndicatorElement);
safeDefine(LiveButtonElement);
safeDefine(MuteButtonElement);
safeDefine(PlayButtonElement);
@@ -6,6 +6,7 @@ import {
error,
icon,
iconState,
playButton,
popup,
root,
slider,
@@ -40,7 +41,11 @@ function getTemplateHTML() {
<div class="${controls}">
<media-tooltip-group>
<div class="${buttonGroup}">
<media-play-button commandfor="play-tooltip" class="${cn(button.base, button.subtle, button.icon, iconState.play.button)}">
<span class="${playButton.wrapper}">
<media-buffering-indicator class="${playButton.bufferingRoot}">
${renderIcon('spinner', { class: icon })}
</media-buffering-indicator>
<media-play-button commandfor="play-tooltip" class="${cn(button.base, button.subtle, button.icon, iconState.play.button, playButton.control)}">
${renderIcon('restart', { class: cn(icon, iconState.play.restart) })}
${renderIcon('play', { class: cn(icon, iconState.play.play) })}
${renderIcon('pause', { class: cn(icon, iconState.play.pause) })}
@@ -49,6 +54,7 @@ function getTemplateHTML() {
<media-tooltip-label></media-tooltip-label>
<media-tooltip-shortcut class="${popup.tooltipShortcut}"></media-tooltip-shortcut>
</media-tooltip>
</span>
<media-live-button class="${cn(button.base, button.subtle, button.live)}"></media-live-button>
</div>
@@ -29,15 +29,20 @@ function getTemplateHTML() {
<div class="media-controls">
<media-tooltip-group>
<div class="media-button-group">
<media-play-button commandfor="play-tooltip" class="media-button media-button--subtle media-button--icon media-button--play">
${renderIcon('restart', { class: 'media-icon media-icon--restart' })}
${renderIcon('play', { class: 'media-icon media-icon--play' })}
${renderIcon('pause', { class: 'media-icon media-icon--pause' })}
</media-play-button>
<media-tooltip id="play-tooltip" side="top" boundary="viewport" class="media-tooltip">
<media-tooltip-label></media-tooltip-label>
<media-tooltip-shortcut class="media-tooltip__kbd"></media-tooltip-shortcut>
</media-tooltip>
<span class="media-button--play__wrapper">
<media-buffering-indicator class="media-buffering-indicator">
${renderIcon('spinner', { class: 'media-icon' })}
</media-buffering-indicator>
<media-play-button commandfor="play-tooltip" class="media-button media-button--subtle media-button--icon media-button--play">
${renderIcon('restart', { class: 'media-icon media-icon--restart' })}
${renderIcon('play', { class: 'media-icon media-icon--play' })}
${renderIcon('pause', { class: 'media-icon media-icon--pause' })}
</media-play-button>
<media-tooltip id="play-tooltip" side="top" boundary="viewport" class="media-tooltip">
<media-tooltip-label></media-tooltip-label>
<media-tooltip-shortcut class="media-tooltip__kbd"></media-tooltip-shortcut>
</media-tooltip>
</span>
<media-live-button class="media-button media-button--subtle media-button--live"></media-live-button>
</div>
@@ -3,6 +3,7 @@
// this entry when building an ejected (light DOM) player layout for live
// HLS / DASH streams.
import { MediaContainerElement } from '../../media/container-element';
import { BufferingIndicatorElement } from '../../ui/buffering-indicator/buffering-indicator-element';
import { LiveButtonElement } from '../../ui/live-button/live-button-element';
import { MuteButtonElement } from '../../ui/mute-button/mute-button-element';
import { PlayButtonElement } from '../../ui/play-button/play-button-element';
@@ -27,6 +28,7 @@ defineVolumeSlider();
defineTime();
// Standalone elements.
safeDefine(BufferingIndicatorElement);
safeDefine(LiveButtonElement);
safeDefine(MuteButtonElement);
safeDefine(PlayButtonElement);
@@ -6,6 +6,7 @@ import {
error,
icon,
iconState,
playButton,
popup,
root,
slider,
@@ -40,15 +41,20 @@ function getTemplateHTML() {
<div class="${controls}">
<media-tooltip-group>
<div class="${buttonGroup}">
<media-play-button commandfor="play-tooltip" class="${cn(button.base, button.subtle, button.icon, iconState.play.button)}">
<span class="${playButton.wrapper}">
<media-buffering-indicator class="${playButton.bufferingRoot}">
${renderIcon('spinner', { class: icon })}
</media-buffering-indicator>
<media-play-button commandfor="play-tooltip" class="${cn(button.base, button.subtle, button.icon, iconState.play.button, playButton.control)}">
${renderIcon('restart', { class: cn(icon, iconState.play.restart) })}
${renderIcon('play', { class: cn(icon, iconState.play.play) })}
${renderIcon('pause', { class: cn(icon, iconState.play.pause) })}
</media-play-button>
<media-tooltip id="play-tooltip" side="top" boundary="viewport" class="${cn(popup.tooltip)}">
<media-tooltip-label></media-tooltip-label>
<media-tooltip-shortcut class="${popup.tooltipShortcut}"></media-tooltip-shortcut>
</media-tooltip>
<media-tooltip-label></media-tooltip-label>
<media-tooltip-shortcut class="${popup.tooltipShortcut}"></media-tooltip-shortcut>
</media-tooltip>
</span>
<media-live-button class="${cn(button.base, button.subtle, button.live)}"></media-live-button>
</div>
+14 -9
View File
@@ -29,15 +29,20 @@ function getTemplateHTML() {
<div class="media-surface media-controls">
<media-tooltip-group>
<div class="media-button-group">
<media-play-button commandfor="play-tooltip" class="media-button media-button--subtle media-button--icon media-button--play">
${renderIcon('restart', { class: 'media-icon media-icon--restart' })}
${renderIcon('play', { class: 'media-icon media-icon--play' })}
${renderIcon('pause', { class: 'media-icon media-icon--pause' })}
</media-play-button>
<media-tooltip id="play-tooltip" side="top" boundary="viewport" class="media-surface media-tooltip">
<media-tooltip-label></media-tooltip-label>
<media-tooltip-shortcut class="media-tooltip__kbd"></media-tooltip-shortcut>
</media-tooltip>
<span class="media-button--play__wrapper">
<media-buffering-indicator class="media-buffering-indicator">
${renderIcon('spinner', { class: 'media-icon' })}
</media-buffering-indicator>
<media-play-button commandfor="play-tooltip" class="media-button media-button--subtle media-button--icon media-button--play">
${renderIcon('restart', { class: 'media-icon media-icon--restart' })}
${renderIcon('play', { class: 'media-icon media-icon--play' })}
${renderIcon('pause', { class: 'media-icon media-icon--pause' })}
</media-play-button>
<media-tooltip id="play-tooltip" side="top" boundary="viewport" class="media-surface media-tooltip">
<media-tooltip-label></media-tooltip-label>
<media-tooltip-shortcut class="media-tooltip__kbd"></media-tooltip-shortcut>
</media-tooltip>
</span>
<media-live-button class="media-button media-button--subtle media-button--live"></media-live-button>
</div>
@@ -2,6 +2,7 @@
// elements without creating a skin element. Use this entry when building an
// ejected (light DOM) player layout for live HLS / DASH streams.
import { MediaContainerElement } from '../../media/container-element';
import { BufferingIndicatorElement } from '../../ui/buffering-indicator/buffering-indicator-element';
import { GestureElement } from '../../ui/gesture/gesture-element';
import { HotkeyElement } from '../../ui/hotkey/hotkey-element';
import { LiveButtonElement } from '../../ui/live-button/live-button-element';
@@ -30,6 +31,7 @@ defineTime();
// Standalone elements.
safeDefine(GestureElement);
safeDefine(HotkeyElement);
safeDefine(BufferingIndicatorElement);
safeDefine(LiveButtonElement);
safeDefine(MuteButtonElement);
safeDefine(PlayButtonElement);
@@ -36,9 +36,7 @@ function getTemplateHTML() {
</media-poster>
<media-buffering-indicator class="${bufferingIndicator.root}">
<div class="${bufferingIndicator.container}">
${renderIcon('spinner')}
</div>
${renderIcon('spinner', { class: icon })}
</media-buffering-indicator>
<media-error-dialog class="${error.root}">
+1 -3
View File
@@ -19,9 +19,7 @@ function getTemplateHTML() {
</media-poster>
<media-buffering-indicator class="media-buffering-indicator">
<div class="media-surface">
${renderIcon('spinner', { class: 'media-icon' })}
</div>
${renderIcon('spinner', { class: 'media-icon' })}
</media-buffering-indicator>
<media-error-dialog class="media-error">
@@ -44,9 +44,7 @@ function getTemplateHTML() {
</media-poster>
<media-buffering-indicator class="${bufferingIndicator.root}">
<div class="${bufferingIndicator.container}">
${renderIcon('spinner')}
</div>
${renderIcon('spinner', { class: icon })}
</media-buffering-indicator>
<media-error-dialog class="${error.root}">
+1 -3
View File
@@ -21,9 +21,7 @@ function getTemplateHTML() {
</media-poster>
<media-buffering-indicator class="media-buffering-indicator">
<div class="media-surface">
${renderIcon('spinner', { class: 'media-icon' })}
</div>
${renderIcon('spinner', { class: 'media-icon' })}
</media-buffering-indicator>
<media-error-dialog class="media-error">
@@ -29,10 +29,17 @@ export interface InputIndicatorCoreApi<IndicatorState extends IndicatorLifecycle
processEvent(event: InputActionEvent, snapshot: MediaSnapshot): boolean;
}
export interface InputIndicatorOptions {
replayOnUpdate?: boolean | undefined;
}
export abstract class InputIndicatorElement<IndicatorState extends IndicatorLifecycleState> extends MediaElement {
protected abstract get core(): InputIndicatorCoreApi<IndicatorState>;
protected abstract get transition(): TransitionApi;
protected abstract get liveIndicator(): LiveIndicator<IndicatorState>;
protected get options(): InputIndicatorOptions {
return {};
}
protected abstract syncCoreProps(): void;
@@ -119,7 +126,12 @@ export abstract class InputIndicatorElement<IndicatorState extends IndicatorLife
this.#snapshot = currentState;
if (this.#lastGeneration !== currentState.generation) {
this.#lastGeneration = currentState.generation;
void this.transition.open();
const transitionState = this.transition.state.current;
if (!transitionState.active || this.options.replayOnUpdate !== false) {
void this.transition.open(this.liveIndicator.element);
} else if (transitionState.status === 'ending') {
this.transition.cancel();
}
}
return;
}
@@ -7,7 +7,7 @@ import {
import { createTransition } from '@videojs/core/dom';
import type { PropertyDeclarationMap } from '@videojs/element';
import { InputIndicatorElement } from '../input-indicators/input-indicator-element';
import { InputIndicatorElement, type InputIndicatorOptions } from '../input-indicators/input-indicator-element';
import { LiveIndicator } from '../input-indicators/live-indicator';
export class VolumeIndicatorElement extends InputIndicatorElement<VolumeIndicatorCore.State> {
@@ -26,6 +26,7 @@ export class VolumeIndicatorElement extends InputIndicatorElement<VolumeIndicato
dataAttrs: VolumeIndicatorDataAttrs,
render: renderVolumeIndicator,
});
readonly #options = { replayOnUpdate: false } satisfies InputIndicatorOptions;
protected get core() {
return this.#core;
@@ -39,6 +40,10 @@ export class VolumeIndicatorElement extends InputIndicatorElement<VolumeIndicato
return this.#liveIndicator;
}
protected override get options() {
return this.#options;
}
protected override syncCoreProps(): void {
this.#core.setProps({ closeDelay: this.closeDelay });
}