NusaPlayer

@nusaplayer/ui

Controls, theme, subtitles, keyboard, screenshot, fullscreen, and toast/notice.

npm i @nusaplayer/core @nusaplayer/ui
<script src="https://cdn.jsdelivr.net/npm/@nusaplayer/core@latest/dist/index.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@nusaplayer/ui@latest/dist/index.min.js"></script>

Basic

import Player from '@nusaplayer/core'
import ui from '@nusaplayer/ui'
 
Player.make('#player', {
  source: {
    title: "Disney's Oceans",
    src: 'https://vjs.zencdn.net/v/oceans.mp4',
    poster: 'https://vjs.zencdn.net/v/oceans.png'
  }
})
  .use([
    ui({
      theme: { primaryColor: '#fb7299', controller: { setting: 'auto' } },
      keyboard: { global: true },
      screenshot: true,
      pictureInPicture: true
    })
  ])
  .create()

UMD: NusaUI(). After create the instance is player.context.ui.

Options

ui({
  theme: {
    primaryColor: '#fb7299',
    watermark: { src: '/logo.png', style: { top: '10px', right: '10px', width: '120px' } },
    progress: { position: 'top' | 'center' | 'auto', mini: true, backward: 10, forward: 10 },
    controller: {
      display: 'always' | 'played',
      header: true | { back: 'always' | 'fullscreen' },
      coverButton: true,
      displayBehavior: 'hover' | 'delay' | 'none',
      slideToSeek: 'none' | 'always' | 'long-touch',
      setting: 'auto' | 'top' | 'bottom'
    }
  },
  autoFocus: true,
  screenshot: false,
  fullscreen: true,
  webFullscreen: true,
  pictureInPicture: true,
  forceLandscapeOnFullscreen: true,
  keyboard: { focused: true, global: false },
  speeds: ['2.0', '1.5', '1.25', '1.0', '0.75', '0.5'],
  settings: ['loop'],
  subtitle: { source: [], fontSize: 20, color: '#fff' },
  thumbnails: { src: '/sprite.jpg', number: 100 },
  highlight: { source: [{ time: 12, text: 'Intro' }] },
  menu: [],
  contextmenu: [],
  icons: {}
})
KeyDefaultNotes
theme.primaryColor#fb7299Progress, volume, active icons
theme.controller.setting'top''auto' puts the gear on the bottom bar (same as desktop)
theme.progress.position'top'Bar sits above the control row
keyboard.globalfalseShortcuts work even when the player is not focused
screenshotfalseSaves a PNG of the current frame
settings['loop']Pass false to hide the extras
contextmenubuilt-infalse keeps the browser menu; an array appends items

Web / native fullscreen on iPhone uses env(safe-area-inset-*) so controls clear the notch and home indicator. The page needs viewport-fit=cover on its viewport meta tag.

Show the title in the header: set source.title and keep theme.controller.header truthy (default).

Subtitles

SRT, WebVTT, and ASS. Cue tags (<b>, <i>, <c.colorRRGGBB>) and ASS overrides (\b, \i, \c, \an) are rendered. Positioned cues follow VTT line / align (and ASS \an).

ui({
  subtitle: {
    source: [
      { name: 'English', default: true, src: '/demo/en.srt' },
      { name: 'English (styled)', src: '/demo/styled.vtt' },
      { name: 'Indonesia', src: '/demo/id.srt' }
    ],
    fontSize: 20,
    color: '#fff',
    bottom: '10%',
    onChange(source) {
      localStorage.setItem('subtitle', source.src)
    }
  }
})
 
player.context.ui.subtitle.changeSource([
  { name: 'English', default: true, src: '/demo/en.srt' }
])

offset on a source shifts cues in seconds.

Thumbnails

Sprite sheet (one image, N frames) or a grid:

thumbnails: { src: '/sprite.jpg', number: 100 }
thumbnails: { src: ['1.jpg', '2.jpg'], x: 10, y: 10, number: 192 }

WebVTT image sprites use vttThumbnails.

Highlights

Markers on the progress bar:

highlight: {
  color: '#fff',
  source: [
    { time: 4, text: 'Intro' },
    { time: 14, text: 'Plugins' },
    { time: 29, text: 'Keyboard' }
  ]
}
 
player.context.ui.changHighlightSource([{ time: 10, text: 'New' }])

Custom menu

ui({
  menu: [
    {
      name: 'Quality',
      key: 'Quality',
      position: 'bottom',
      anchor: 'settings',
      children: [
        { name: 'FHD', default: true, value: 'https://vjs.zencdn.net/v/oceans.mp4' },
        { name: 'HLS', value: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8' }
      ],
      onChange({ value }) {
        player.changeQuality({ src: value })
      }
    }
  ]
})
 
player.context.ui.menu.register(menu)
player.context.ui.menu.unregister('Quality')
player.context.ui.menu.select('Quality', 0)
player.context.ui.menu.updateLabel('Quality', '1080P')

anchor: 'settings' places the control next to the gear (used for quality on desktop).

Context menu

Right-click the player (desktop) for play, mute, loop, screenshot, PiP, fullscreen, and copy URL. About NusaPlayer opens an overlay and the GitHub repo. Mobile keeps the native menu so it does not fight long-press seek.

ui({
  contextmenu: [
    {
      key: 'docs',
      name: 'Docs',
      onClick() {
        window.open('https://nusaplayer.vercel.app/docs', '_blank')
      }
    }
  ]
})
 
player.context.ui.contextmenu.register({ key: 'reload', name: 'Reload', onClick: () => player.load() })
player.context.ui.contextmenu.unregister('docs')

contextmenu: false restores the browser menu.

Custom setting

ui({
  settings: [
    'loop',
    {
      key: 'theme',
      type: 'selector',
      name: 'Theme',
      children: [
        { name: 'Pink', value: '#fb7299', default: true },
        { name: 'Blue', value: '#3b82f6' }
      ],
      onChange({ value }) {
        document.documentElement.style.setProperty('--primary', value)
      }
    }
  ]
})
 
player.context.ui.setting.register(setting)
player.context.ui.setting.unregister('theme')
player.context.ui.setting.updateLabel('theme', 'Pink')
player.context.ui.setting.select('theme', 0)

Types: selector, switcher, slider, option.

Notice & toast

player.context.ui.notice('Saved', 'top')
player.emit('notice', { text: 'Saved', pos: 'top' })
 
player.context.ui.toast('Switched to 1080P')
player.context.ui.toast({
  title: 'Delete history?',
  text: 'This cannot be undone.',
  type: 'warning',
  duration: false,
  actions: [
    { text: 'Cancel', variant: 'ghost' },
    { text: 'Delete', variant: 'danger', onClick: () => {} }
  ]
})
player.context.ui.toast.closeAll()

Positions: top, bottom, left, right, center, top-left, top-center, top-right, left-bottom, bottom-right.

Keyboard

Enabled when keyboard.focused (player focused) or keyboard.global.

KeyAction
/ Volume ±10%
/ Seek ±5s
Space / EnterPlay / pause
SScreenshot
FFullscreen
WWeb fullscreen
MMute
CSettings
LPlaylist (if the playlist plugin is loaded)
player.context.ui.keyboard.register({
  KeyK: () => player.togglePlay()
})
player.context.ui.keyboard.unregister(['KeyK'])

Watermark

Position with top / left / right / bottom on style so screenshots include it.

ui({
  theme: {
    watermark: {
      src: '/logo.png',
      style: { top: '10px', right: '10px', width: '160px', height: 'auto' },
      attrs: { class: 'watermark', crossOrigin: 'anonymous' }
    }
  }
})

UI events

controlsshown, controlshidden, backward, previous, next.

Wire skip buttons with previous / next (the playlist plugin already listens).

Errors

player.emit('error', { message: 'Stream failed', code: 1 })
 
ui({
  errorBuilder(error, target, fallback) {
    fallback(error)
  }
})