Plugin: Desktop UI
Everything on the Build a Player tutorial is code you can write yourself against the typed method surface. DesktopUiPlugin (id 'desktop-ui') is the alternative: a complete prebuilt overlay, top bar, center play button, bottom bar with scrubber and transport, a settings menu, an auto-hide activity cycle, still built from the same public methods (play(), subtitle(), quality()) and still restylable through plain CSS, not a black box.
Options
| Option | Type | Default | Purpose |
|---|---|---|---|
hideTitle | boolean | false | Hide the top-bar title text. |
disableClickToPause | boolean | false | Stop a click on the video element from toggling playback. |
inactivityMs | number | 4000 | Auto-hide delay in milliseconds for the overlay after the last pointer activity. |
imageBaseUrl | string | — | Base URL prefix for relative playlist-menu thumbnail paths, separate from the player’s own baseImageUrl. |
buttons | DesktopUiButtonOptions | — | Per-button visibility overrides, see below. |
buttonOrder | Array<keyof DesktopUiButtonOptions> | — | Re-anchors named buttons to the end of the bar in the given order; unnamed buttons keep their natural position. |
settingsItems | SettingsToggleItem[] | — | Consumer toggle rows appended to the settings menu (an app’s own auto-skip switch, for instance). |
subtitleMenuActions | SubtitleMenuAction[] | — | Consumer action row(s) appended to the subtitles sub-menu (a “Search subtitles online…” entry, for instance). Shown whenever provided, including when the current item has zero subtitle tracks, since that’s exactly when an external-search action matters most. |
buttonPriority | Array<keyof DesktopUiButtonOptions> | see below | Responsive removal order, buttons at the end are hidden first as the container narrows. |
breakpoints | Breakpoint[] | — | Full custom breakpoint progression. Takes precedence over collapseStages. |
collapseStages | [number, number, number] | — | Shorthand for hideAfterRank at the sm/md/lg tiers, ignored when breakpoints is set. |
volumeSlider | 'horizontal' | 'vertical' | 'auto' | 'auto' | Slider orientation; 'auto' uses the vertical popup on touch devices and at 520px or narrower player width, horizontal otherwise. |
Button visibility
DesktopUiButtonOptions controls which control-bar buttons render at all, independent of the responsive priority system below.
Default-on: play, mute, volume, fullscreen, settings, next, previous, chapterPrev, chapterNext. Chapter buttons hide automatically when the current item has no chapters; next / previous stay rendered but go disabled at the first / last item and whenever the queue holds a single item.
Default-off (opt in via buttons: { theater: true }, etc.): theater, pip, speed, quality, subtitles, audio, playlist, seekBack, seekForward, aspectRatio, cast. seekBack/seekForward default off because a quick seek is already reachable elsewhere: the keyboard’s bare ArrowLeft/ArrowRight (5s, KeyHandlerPlugin’s default rewind()/forward() step) and touch zones’ double-tap (10s by default, its own seekSeconds option), the control-bar buttons are a deliberate opt-in for a UI with no keyboard or touch input at all.
The top-bar cast button (buttons: { cast: true }, next to back/close) only emits the player’s 'cast' event on click, the same no-opinion pattern as back/close. Unlike those two, its visibility does not depend on a listener being registered, since it defaults off rather than on. Wire a listener to open your own device picker; never route the click through CastSenderPlugin or session.loadMedia() directly, that plugin drives an active cast session once a target is already chosen, it doesn’t own device selection.
import nmplayer from '@nomercy-entertainment/nomercy-video-player';
const player = nmplayer('player');
player.addPlugin(DesktopUiPlugin, {
buttons: { theater: true, subtitles: true, audio: true },
});
Responsive collapsing
As the player container narrows, buttons at the end of buttonPriority (default order: play -> mute -> volume -> fullscreen -> settings -> next -> previous -> chapterPrev -> chapterNext -> seekBack -> seekForward -> theater -> pip -> speed -> quality -> subtitles -> audio -> aspectRatio -> playlist) drop off first, so the most essential buttons survive longest. collapseStages: [2, 4, 6] is a shorthand for hiding after rank 2/4/6 at the sm/md/lg breakpoints (xs always shows rank 1 only, xl always shows all); breakpoints gives full control over both the pixel thresholds and the rank cutoffs when the shorthand doesn’t fit:
player.addPlugin(DesktopUiPlugin, {
breakpoints: [
{ name: 'xs', maxWidth: 320, hideAfterRank: 1 },
{ name: 'sm', maxWidth: 480, hideAfterRank: 4 },
{ name: 'lg', maxWidth: Infinity, hideAfterRank: Infinity },
],
});
'layout:breakpoint' fires on every transition with { from, to, visibleButtons, hiddenButtons }, a consumer building its own overflow menu subscribes here instead of re-deriving visibility from container width itself.
Portrait
Portrait fits about five or six controls whatever the priority list says, so a set is forced off there regardless of available width. Which set is yours to choose: portraitHidden replaces the default (chapterPrev, chapterNext, previous, next, subtitles, audio, quality, playlist) rather than merging with it, so name every button you want gone. An episodic app usually wants next and chapterNext to survive, since skipping an intro one-handed is the reason a phone viewer reaches for the controls at all:
player.addPlugin(DesktopUiPlugin, {
buttons: { chapterNext: true, next: true },
// Keep next/chapterNext in portrait, drop their backwards twins instead.
portraitHidden: ['chapterPrev', 'previous', 'subtitles', 'audio', 'quality', 'playlist'],
});
Pass [] to force nothing off and let width alone decide. The override buys a button a seat in the priority walk, it does not exempt it: a button still disappears once the row runs out of room, so raise it in buttonPriority too if it must outlive the squeeze.
The overlay() escape hatch
Other plugins mount their own UI onto DesktopUiPlugin’s auto-hiding overlay root instead of the player container directly, so their elements inherit the same show/hide lifecycle rather than floating over a hidden chrome:
const myCustomBadge = document.createElement('div'); // your own overlay element
const overlayRoot = player.getPlugin(DesktopUiPlugin)?.overlay(); // HTMLElement | null
if (overlayRoot) {
overlayRoot.appendChild(myCustomBadge);
}
Returns null before use() has built the DOM, guard the call rather than assuming it’s always populated.
An overlay anchored to the top or bottom bar, a device picker under the cast button, say, must also stop those bars auto-hiding underneath it while it’s open. holdChrome() pins them visible; releaseChrome() lets the countdown resume. The hold is counted, so independent holders compose and the chrome stays until the last release; call them in balanced pairs:
const ui = player.getPlugin(DesktopUiPlugin);
ui?.holdChrome(); // opening your overlay
// …
ui?.releaseChrome(); // closing it
This plugin’s own menus don’t need it, they already pin internally. It’s for UI that lives outside the plugin but leans on its chrome.
Rules & restrictions
- Loading
DesktopUiPluginalongside nativecontrols: trueorTvKeyHandlerPlugin’s own key bindings without disabling one results in doubled UI or conflicting handlers, pick one control surface per platform. - Menu-driven vs. cycle-driven is a deliberate split: quality, subtitles, audio, speed, and aspect ratio open a menu on click (multi-state, needs a list); theater, PiP, and fullscreen are direct binary toggles on click.
cycleAspectRatio()and friends exist for remote-control / key-bind contexts where the user can’t pick from a list, not as the pointer-input default. disable(reason?)hides the overlay outright (not just its handlers) and cancels the inactivity timer, the pattern a disc-menu interpreter or another plugin that needs to own the screen uses to take over without doubling chrome.enable()restores it and re-arms auto-hide.- Built from
composeMixins, the same primitiveNMVideoPlayeritself is composed from, nine mixin groups (feedback, shortcuts, activity, menus, icon state, transport state, chapters, sprites, DOM) stamped onto one prototype rather than one enormous class body.
How to extend it
Styling goes through plain CSS against the plugin’s own class names, appendStyles() loads styles.css once per page regardless of player count, override selectors in your own stylesheet rather than forking the plugin. For behavior beyond what options cover, subclass and override the lifecycle hooks or add your own plugin that reads overlay():
import { DesktopUiPlugin } from '@nomercy-entertainment/nomercy-video-player/plugins/desktop-ui';
player.addPlugin(DesktopUiPlugin, {
buttons: { theater: true, quality: true },
volumeSlider: 'auto',
settingsItems: [
{ id: 'auto-skip', label: () => 'Auto-skip intros', get: () => autoSkip.value, set: (v) => { autoSkip.value = v; } },
],
});
Next steps
- Plugin: Touch Zones: the touch-input counterpart, coordinated with this plugin’s auto-hide cycle through the shared
'activity'event.