Skip to content

Desktop UI

DesktopUiPlugin is the full video player controls overlay for pointer-driven environments. Register it as the primary UI plugin for web desktop and mobile PWA deployments. It mounts a complete DOM control set, including a top bar, progress bar with chapter markers and sprite thumbnail preview, control buttons, and menu panes.

Plugin id

'desktop-ui'

Import

TypeScript
import { DesktopUiPlugin } from '@nomercy-entertainment/nomercy-video-player/plugins';
import type {
DesktopUiOptions,
} from '@nomercy-entertainment/nomercy-video-player/plugins';

DesktopUiButtonOptions, DesktopUiEvents, Breakpoint, ButtonPriorityList, and LayoutBreakpointPayload are defined in the plugin source but are not re-exported from the /plugins barrel. Import them from the deep path if you need the types directly:

TypeScript
import type {
DesktopUiButtonOptions,
DesktopUiEvents,
Breakpoint,
ButtonPriorityList,
LayoutBreakpointPayload,
} from '@nomercy-entertainment/nomercy-video-player/plugins/desktop-ui';

What it does

DesktopUiPlugin renders the complete video player controls overlay as a DOM tree mounted inside .nomercyplayer. It is a v2-native rewrite of the v1 desktop UI and provides:

  • Top bar with title, show/season/episode info, back button, and close button
  • Progress bar with chapter markers, segmented buffer fill, scrubber, hover-time label, and sprite thumbnail preview
  • Control bar with play/pause, navigation, skip buttons, volume slider (horizontal expand-on-hover or vertical popup), speed selector, subtitle/audio/quality menus, theater, PiP, and fullscreen
  • Menu panes for speed, quality, subtitles, audio, playlist, subtitle style settings, and aspect ratio
  • Seek-feedback animations
  • Activity detection: controls auto-hide after inactivity, tap or move to reveal
  • Keyboard shortcut sheet toggled with ?

The back button in the top bar only renders when at least one 'back' event listener is registered. The close button only renders when at least one 'close' event listener is registered.

Hover thumbnails on the progress bar come from the active item’s previewSpriteUrl (a WebVTT sprite manifest). A legacy tracks entry with kind: 'thumbnails' still works as a fallback for un-normalized v1 items. No extra configuration is needed.

CSS state classes

State CSS classes are applied to .nomercyplayer (the player container element) by the player core’s container-class-emit mixin:

StateCSS class on .nomercyplayer
Playingplaying
Pausedpaused
Stoppedstopped
Endedended
Loadingloading
Bufferingbuffering
Mutedmuted
Fullscreenfullscreen
PiP activepip
Theater modetheater
Controls visible (activity)active
Controls hidden (idle)inactive

The playback-state classes (playing, paused, stopped, ended, loading, buffering) are mutually exclusive. The player core removes all others before adding the new one.

The container also receives data-orientation (portrait or landscape) and data-breakpoint (the active responsive tier name) as attributes.

Inactivity hiding

Controls auto-hide after 4 seconds of inactivity (configurable via inactivityMs). Mouse movement, keyboard input, or touch interaction resets the timer. Controls stay visible while paused, while a menu is open, or while the pointer is hovering over the bottom bar or menu frame.

Options

OptionTypeDefaultDescription
hideTitlebooleanfalseHide the title column in the top bar (title and show info). The back and close buttons remain.
disableClickToPausebooleanfalseSuppress click/tap on the video area toggling pause.
inactivityMsnumber4000Milliseconds of pointer/keyboard inactivity before controls hide.
imageBaseUrlstringnoneBase URL prepended to relative image paths in the playlist pane.
buttonsDesktopUiButtonOptionssee belowPer-button visibility overrides.
buttonPriorityButtonPriorityListsee belowPriority order for responsive button removal. Buttons at the end are hidden first as the container narrows.
breakpointsBreakpoint[]see belowFull breakpoint progression for the layout:breakpoint event and data-breakpoint attribute.
collapseStages[number, number, number]noneShorthand for breakpoints. Provide hideAfterRank values for the sm/md/lg tiers. Ignored when breakpoints is provided.
volumeSlider'horizontal' | 'vertical' | 'auto''auto'Volume slider orientation. auto uses vertical popup on narrow containers (at or below 520 px) and touch-only devices.

DesktopUiButtonOptions

All fields are boolean | undefined. Omitting a field keeps the button’s own default.

Default-ON buttons (visible unless set to false):

ButtonDescription
playPlay/pause toggle
muteMute toggle icon
volumeVolume slider
fullscreenFullscreen toggle
settingsSettings/gear button
nextNext queue item
previousPrevious queue item
chapterPrevPrevious chapter (content-gated: hidden when the current item has no chapters)
chapterNextNext chapter (content-gated: hidden when the current item has no chapters)

Default-OFF buttons (hidden unless set to true):

ButtonDescription
theaterTheater mode toggle
pipPicture-in-Picture toggle
speedPlayback speed menu
qualityQuality level menu (content-gated: hidden when fewer than 2 levels are available)
subtitlesSubtitle track menu (content-gated: hidden when no subtitle tracks are present)
audioAudio track menu (content-gated: hidden when fewer than 2 audio tracks are present)
playlistPlaylist panel (content-gated: hidden when queue has fewer than 2 items)
seekBackSeek backward 10 s
seekForwardSeek forward 10 s
aspectRatioAspect ratio menu

seekBack/seekForward default to false because ±10 s seek is already available via touch zones (double-tap) and keyboard arrow keys (KeyHandlerPlugin). The chapter navigation buttons are the unique control-bar value in the default layout.

Responsive button removal

Button visibility is decided by fit math, not by fixed breakpoint tiers. The plugin walks the priority list most-important-first, adds up each shown button’s width, and hides any button that no longer fits the available width (the container width minus space reserved for the time labels and divider). So as the container narrows, buttons drop off in reverse priority order, the most optional first:

play -> mute -> volume -> fullscreen -> settings -> next -> previous -> chapterPrev -> chapterNext -> seekBack -> seekForward -> theater -> pip -> speed -> quality -> subtitles -> audio -> aspectRatio -> playlist

Override with buttonPriority if you need a different order.

The plugin also reports a breakpoint name on the layout:breakpoint event and as a data-breakpoint attribute on the container, derived from these thresholds. These help consumers react to width tiers; they do not themselves gate which buttons render (the fit math does):

BreakpointMax container width
xs320 px
sm480 px
md720 px
lg1024 px
xlno limit

Portrait mode additionally hides chapterPrev, chapterNext, previous, next, subtitles, audio, quality, and playlist regardless of container width.

Events

EventPayloadDescription
'plugin:desktop-ui:shortcuts-toggle'undefinedKeyboard shortcut sheet toggled (via ? key or by emitting the event).
'plugin:desktop-ui:layout:breakpoint'LayoutBreakpointPayloadFires when the active responsive breakpoint tier changes.
'plugin:desktop-ui:opts:changed'DesktopUiOptionsFires when options are updated at runtime.

The plugin also triggers the player-level 'activity' event ({ active: boolean }) to signal controls show/hide state to other plugins. That event is on the player, not namespaced under desktop-ui.

TypeScript
// React to breakpoint changes (for custom layout logic):
player.on('plugin:desktop-ui:layout:breakpoint', ({ from, to, visibleButtons, hiddenButtons }) => {
console.log(`breakpoint changed from ${from} to ${to}`);
});

// Trigger the shortcut sheet from outside the plugin:
player.emit('plugin:desktop-ui:shortcuts-toggle', undefined);

Registration

TypeScript
import nmplayer from '@nomercy-entertainment/nomercy-video-player';
import {
DesktopUiPlugin,
KeyHandlerPlugin,
SubtitleOverlayPlugin,
} from '@nomercy-entertainment/nomercy-video-player/plugins';

const player = nmplayer('player')
.addPlugin(DesktopUiPlugin, {
buttons: {
theater: true,
pip: true,
subtitles: true,
audio: true,
quality: true,
},
volumeSlider: 'auto',
})
.addPlugin(KeyHandlerPlugin)
.addPlugin(SubtitleOverlayPlugin)
.setup({
baseUrl: 'https://raw.githubusercontent.com/NoMercy-Entertainment/nomercy-media/master/Films',
playlist: [
{
id: 1,
title: 'Sintel',
url: '/Sintel.(2010)/Sintel.(2010).NoMercy.m3u8',
image: 'https://image.tmdb.org/t/p/w780/q2bVM5z90tCGbmXYtq2J38T5hSX.jpg',
previewSpriteUrl: '/Sintel.(2010)/thumbs_256x109.vtt',
chapters: [
{ index: 0, title: 'Intro', start: 0, end: 90 },
{ index: 1, title: 'Act 1', start: 90, end: 600 },
],
},
],
});

// The back button only renders when this listener is registered:
player.on('back', () => {
history.back();
});

DOM tree

The plugin builds its tree inside .nomercyplayer:

ElementParentContents / Children
overlay.nomercyplayerRoot overlay container
top-baroverlayBack button, close button, title column
centeroverlaySpinner, center play/pause button
bottom-baroverlayShadow, slider row, control row
bottom-bar-shadowbottom-barVisual shadow gradient
top-row (slider-bar)bottom-barslider-buffer, slider-hover, slider-progress, chapter-progress x N, slider-nipple, slider-pop (sprite thumbnail preview)
bottom-rowbottom-barLeft controls (play, prev, rewind, forward, chapterPrev, chapterNext, next), volume-container (mute + slider), current-time, remaining-time, right controls (aspectRatio, theater, pip, speed, subtitles, audio, quality, playlist, settings, fullscreen)
menu-frame-dialogoverlayMenu panes (speed, quality, subtitles, audio, playlist, subtitle style, aspect ratio)

Default keyboard shortcuts

KeyHandlerPlugin must be registered separately via addPlugin(KeyHandlerPlugin). It is not bundled inside DesktopUiPlugin. The shortcuts sheet (press ?) shows all bindings once both plugins are registered.

KeyAction
SpaceToggle playback
SStop
EFrame advance
] / [Speed up / speed down
=Reset to normal speed
/ Volume up / down
MToggle mute
/ Seek ±5 seconds
Shift + / Seek ±3 seconds
Alt + / Seek ±10 seconds
Ctrl + / Seek ±60 seconds
3 / 6 / 9 / 1Jump 30/60/90/120 seconds
N / PNext / previous queue item
Shift + N / PNext / previous chapter
VCycle subtitle tracks
BCycle audio tracks
ACycle aspect ratio
+ / -Subtitle size up / down
F / F11Toggle fullscreen
EscExit fullscreen
TShow current time and time remaining as an OSD message
?Toggle shortcut sheet

Customizing the UI

The UI uses standard DOM elements styled by the player’s CSS. Override styles by targeting .nomercyplayer and its children in your app stylesheet.

To replace the entire UI, subclass DesktopUiPlugin and pass your subclass to addPlugin. DesktopUiPlugin has no protected hook methods to override; use addPlugin(MyDesktopUi, opts) with the same options contract:

TypeScript
import { DesktopUiPlugin } from '@nomercy-entertainment/nomercy-video-player/plugins';

class MyDesktopUi extends DesktopUiPlugin {
static override readonly id = 'desktop-ui';

override use(): void {
super.use();
// attach extra DOM or event listeners after the base UI is mounted
}
}

player.addPlugin(MyDesktopUi);

TV UI

For 10-foot TV remote navigation, use TvKeyHandlerPlugin instead. It is optimized for D-pad navigation and does not mount the pointer-based desktop controls.

TypeScript
import { TvKeyHandlerPlugin } from '@nomercy-entertainment/nomercy-video-player/plugins';

player.addPlugin(TvKeyHandlerPlugin);

UX rules

  • Click on multi-state buttons (quality, subtitles, audio, speed, aspect ratio) opens a menu pane. The cycle actions (cycleAspectRatio, cycleSubtitles) are for keyboard and remote-control contexts where the user cannot pick from a list.
  • Theater, PiP, and Fullscreen are binary toggles: direct action on click.
  • Menus close when clicking outside the menu frame, or when another menu opens.

i18n

DesktopUiPlugin ships en and nl translation bundles covering every tooltip, menu label, and shortcut sheet string. All keys use the plugin.desktop-ui.* namespace.

The plugin registers its bundles automatically when it loads, so no manual setup is needed for en and nl. To override a label or add a third language, pass the keys through the player’s translations config:

TypeScript
import { defaultTranslations } from '@nomercy-entertainment/nomercy-player-core';

player.setup({
language: 'fr',
translations: {
...defaultTranslations,
fr: {
'plugin.desktop-ui.tooltip.play': 'Lecture / Pause',
'plugin.desktop-ui.tooltip.fullscreen': 'Plein écran',
'plugin.desktop-ui.tooltip.subtitles': 'Sous-titres',
'plugin.desktop-ui.menu.quality': 'Qualité',
'plugin.desktop-ui.menu.auto': 'Automatique',
'plugin.desktop-ui.menu.off': 'Désactivé',
},
},
playlist: [/* ... */],
});

Key categories in the plugin.desktop-ui.* namespace:

CategoryExample keys
tooltip.*tooltip.play, tooltip.fullscreen, tooltip.subtitles, tooltip.audio, tooltip.quality
shortcuts.*shortcuts.title, shortcuts.playPause, shortcuts.seekBack5, shortcuts.volumeUp
shortcuts.group.*shortcuts.group.playback, shortcuts.group.seeking, shortcuts.group.volume
menu.*menu.settings, menu.quality, menu.audio, menu.subtitles, menu.auto, menu.off
menu.subtitle.*menu.subtitle.font, menu.subtitle.textSize, menu.subtitle.textColor

To switch language at runtime:

TypeScript
await player.language('nl');

The Dutch bundle is already registered; the player switches immediately. For any other language, provide the bundle via translations or loadTranslations.

See also