@elementmints/veedeeoh
v1.0.0
Published
Production-grade, framework-agnostic, accessible HTML5 video player — vanilla JS (ES modules) + SCSS + SVG sprite. Custom controls, lazy load, HLS, captions, i18n/RTL, gestures, plugins.
Maintainers
Readme
@elementmints/veedeeoh
A production-grade, framework-agnostic, modular HTML5 video player written in vanilla JS (ES modules) + SCSS + an SVG sprite. No Shadow DOM; fully custom controls; CSP-safe. Works with any bundler/framework (and is consumed by the MG Motors AEM site as a local source package).
Install
npm install @elementmints/veedeeohPeer/runtime deps hls.js, focus-trap and @floating-ui/dom are installed
automatically.
Quick start
import VideoPlayer, { setSpritePath } from '@elementmints/veedeeoh';
import '@elementmints/veedeeoh/styles.css';
// Point at the bundled icon sprite (serve it from your app, or set a CDN URL).
setSpritePath('/assets/veedeeoh-sprite.svg'); // ships at @elementmints/veedeeoh/sprite.svg
const player = new VideoPlayer({
root: document.querySelector('.my-video'),
sources: { desktop: [{ src: '/clip-720.mp4', type: 'video/mp4', quality: 720 }] },
poster: { desktop: '/poster.jpg', alt: 'Clip' }
});The root element should contain (or the player will create) a
.cmp-video-player__stage > video.cmp-video-player__video; a
<picture class="cmp-video-player__poster"> is optional.
Architecture
@elementmints/veedeeoh/src/
index.js Public barrel (default export: VideoPlayer)
types.d.ts Public TypeScript declarations
constants.js Events, DI tokens, default keybindings, class/attr names
core/ VideoPlayer, Config, EventBus, StateStore, Ticker,
ServiceContainer (DI), PluginManager, PlayerElement,
mediaEvents, fullscreen
providers/ Provider contract + Html5/NativeHls/Hls + ProviderFactory
+ capabilities (UA/feature detection)
controls/ ControlBar, ControlRegistry, BaseControl, primitives
(Button/Slider/Menu) and one module per control
tooltip/ Keyboard-accessible TooltipManager (@floating-ui)
accessibility/ A11yManager, LiveRegion, focusTrap (focus-trap)
keyboard/ KeyboardManager + configurable keymap
gestures/ GestureManager + recognizers (double-tap/swipe/pinch/hold)
visibility/ VisibilityManager (pause out of view)
lazyload/ LazyLoader (IntersectionObserver, configurable)
single-active/ ActiveVideoRegistry (page-wide single active video)
slider/ SliderManager + Swiper/Slick/Splide/Embla/Modal/Custom
i18n/ I18nManager + localeLoader + locales (en/hi/ar)
icons/ IconRegistry + sprite resolver + default icon map
analytics/ AnalyticsManager + dataLayer/console adapters
plugins/ Plugin contract + example plugins
security/ dom.js (the only DOM builder) + security.js (sanitize/URL)
styles/ SCSS: tokens, player, controls, slider, menu, tooltip,
themes, a11y, rtlPrinciples: SOLID, composition over inheritance, an event bus for all intra-player messaging, a plugin architecture with lifecycle hooks, and a lightweight DI container so managers are swappable and unit-testable.
Usage
Full configuration example
import VideoPlayer from '@elementmints/veedeeoh';
const player = new VideoPlayer({
root: document.querySelector('.video'),
lazyLoad: true,
lazyThreshold: 0.25,
pauseOutOfView: true,
resumeInView: false,
singleActiveVideo: true,
language: 'en',
poster: { desktop: '/poster.jpg', mobile: '/poster-m.jpg', alt: 'MG Comet' },
sources: {
desktop: [
{ src: '/comet-1080.mp4', type: 'video/mp4', quality: 1080 },
{ src: '/comet-720.mp4', type: 'video/mp4', quality: 720 }
],
mobile: [{ src: '/comet-m.mp4', type: 'video/mp4' }]
},
controls: {
position: 'bottom',
order: ['play', 'seek', 'current-time', 'duration', 'mute', 'volume',
'spacer', 'settings', 'fullscreen']
},
gestures: true,
keyboard: true
});
player.on('play', () => {});VideoPlayer.init(root) initialises every [data-cmp="media"] under root,
reading per-element config from a data-vp-config JSON attribute.
Events
Emitted on the internal bus and mirrored as vp:<name> DOM CustomEvents:
ready, mounted, play, playing, pause, ended, timeupdate, progress, waiting,
canplay, seeking, seeked, ratechange, volumechange, qualitychange, levelsupdated,
fullscreenchange, pipchange, captionschange, visibilitychange, languagechange,
gesture, slidechange, next, previous, error, destroyed.
Plugins
import { createPlugin } from '@elementmints/veedeeoh';
player.use(createPlugin({
name: 'my-plugin',
beforeInit(ctx) { ctx.registerControl('badge', (c) => myControl(c)); },
onReady(ctx) {}
}));Hooks: beforeInit / afterInit / onReady / onDestroy. Registration surface:
registerControl, registerProvider, registerSliderAdapter, registerAnalyticsAdapter.
Slider integration
Auto-detects Swiper, Slick, Splide, Embla, Bootstrap modal and a configurable
custom slider. Only the active, fully-visible slide may play; the rest pause. Set
slider.adapter to force one, or 'custom' with slider.event/slider.activeClass.
Strategies
- Accessibility (WCAG 2.2 AA): semantic roles, ARIA labels, full keyboard operability, visible focus rings, fullscreen focus trap, a polite live region, reduced-motion and forced-colors support, RTL.
- Security (VAPT): no internal
innerHTML(all DOM viasecurity/dom.js), source-URL protocol allow-list, author HTML sanitised via DOMPurify when present else a DOMParser allow-list, CSS-variable theming (CSP-safe). - Performance: one shared rAF Ticker, passive listeners, IntersectionObserver
lazy load + visibility,
requestIdleCallbackfor deferrable work, GPU-friendly compositing, full teardown (destroy()) that removes every listener/observer and releases media buffers;hls.jsis a lazy chunk fetched only for HLS sources.
Browser support
| Browser | Notes |
|---|---|
| Chrome / Edge (last 2) | Full support |
| Firefox (last 2) | Full support |
| Safari 14+ macOS | Native HLS; element fullscreen for video |
| iOS Safari 14+ | playsinline, muted autoplay, native HLS, element fullscreen, no PiP button |
| Android Chrome | Full support |
Build, test & publish
npm install
npm test # Jest (jsdom) — co-located src/**/*.test.js
npm run build # dist/veedeeoh.js (ESM) + .umd.cjs + veedeeoh.css
npm publish --access public # requires `npm login` + @elementmints org accessexports: . → built ESM/UMD (+ types), ./styles.css → compiled CSS,
./scss → SCSS source, ./sprite.svg → the icon sprite.
Demo
A runnable demo covering every scenario lives in demo/ (npx vite from there).
Using it inside the MG AEM monorepo
The AEM app (ui.frontend) consumes this package's source via a webpack +
Jest alias (@elementmints/veedeeoh → packages/veedeeoh/src), so no build step
is needed for the site build. To switch to the published registry version, drop
the alias and add "@elementmints/veedeeoh": "^1.0.0" to ui.frontend's deps.
