Video Player: Vanilla JS
No framework is required to use the player. You can load it from a CDN with a single <script> tag, or import it directly in any bundler-based project (Vite, Rollup, Webpack, esbuild).
CDN / script-tag path
Load hls.js first, then the player IIFE bundle. The bundle reads Hls from window, so the order matters.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style>
#player {
width: 100%;
max-width: 960px;
aspect-ratio: 16 / 9;
background: #000;
}
</style>
</head>
<body>
<div id="player"></div>
<div class="controls">
<button id="play-btn">Play</button>
<span id="time-display">0s / 0s</span>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/hls.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@nomercy-entertainment/nomercy-video-player@beta/dist/nomercy-video-player.iife.js"></script>
<script>
var player = window.nmplayer('player').setup({
baseUrl: 'https://raw.githubusercontent.com/NoMercy-Entertainment/nomercy-media/master/Films',
playlist: [
{
id: 'sintel',
title: 'Sintel',
url: '/Sintel.(2010)/Sintel.(2010).NoMercy.m3u8',
image: 'https://image.tmdb.org/t/p/w780/q2bVM5z90tCGbmXYtq2J38T5hSX.jpg',
duration: 888,
subtitles: [
{
id: '0',
label: 'English',
url: '/Sintel.(2010)/subtitles/Sintel.(2010).NoMercy.eng.full.vtt',
language: 'eng',
kind: 'subtitles',
},
],
},
],
});
var playBtn = document.getElementById('play-btn');
var timeDisplay = document.getElementById('time-display');
player.on('ready', function () {
player.item(0, { autoplay: true });
});
player.on('time', function (data) {
timeDisplay.textContent = Math.floor(data.time) + 's';
});
player.on('play', function () {
playBtn.textContent = 'Pause';
});
player.on('pause', function () {
playBtn.textContent = 'Play';
});
playBtn.addEventListener('click', function () {
player.togglePlayback();
});
</script>
</body>
</html>
window.nmplayer is the factory function exposed by the IIFE bundle. Pass it the id of your container element and chain .setup(config).
Note: Built-in plugins (such as
DesktopUiPlugin) are not included in the IIFE bundle. To use them you need the ESM path with a bundler, described in the next section.
Bundler / ESM path
Install the package (see Installation), then import and initialize after the DOM is ready:
import nmplayer from '@nomercy-entertainment/nomercy-video-player';
import { DesktopUiPlugin, KeyHandlerPlugin } from '@nomercy-entertainment/nomercy-video-player/plugins';
import type { VideoPlayerConfig, VideoPlaylistItem } from '@nomercy-entertainment/nomercy-video-player';
const items: VideoPlaylistItem[] = [
{
id: 'sintel',
title: 'Sintel',
url: '/Sintel.(2010)/Sintel.(2010).NoMercy.m3u8',
image: 'https://image.tmdb.org/t/p/w780/q2bVM5z90tCGbmXYtq2J38T5hSX.jpg',
duration: 888,
subtitles: [
{
id: '0',
label: 'English',
url: '/Sintel.(2010)/subtitles/Sintel.(2010).NoMercy.eng.full.vtt',
language: 'eng',
kind: 'subtitles',
},
],
},
];
const config: VideoPlayerConfig = {
baseUrl: 'https://raw.githubusercontent.com/NoMercy-Entertainment/nomercy-media/master/Films',
playlist: items,
};
const player = nmplayer('player')
.addPlugin(DesktopUiPlugin)
.addPlugin(KeyHandlerPlugin)
.setup(config);
player.on('ready', () => {
player.item(0, { autoplay: true });
});
addPlugin accepts a plugin class, not an instance. Chain as many plugins as you need before calling setup.
Wiring transport controls
Once you have a player instance, any DOM button or keyboard listener can call methods directly:
const playBtn = document.getElementById('play-btn') as HTMLButtonElement;
const timeDisplay = document.getElementById('time-display') as HTMLSpanElement;
const seekBar = document.getElementById('seek-bar') as HTMLInputElement;
// Update the button label on play/pause state changes
player.on('play', () => {
playBtn.textContent = 'Pause';
});
player.on('pause', () => {
playBtn.textContent = 'Play';
});
// Drive a custom seek bar from the time event
player.on('time', ({ time }) => {
timeDisplay.textContent = Math.floor(time) + 's';
const duration = player.duration();
if (duration > 0) {
seekBar.value = String((time / duration) * 100);
}
});
// Button clicks call player methods
playBtn.addEventListener('click', () => {
player.togglePlayback();
});
seekBar.addEventListener('input', () => {
const pct = Number(seekBar.value);
player.seekByPercentage(pct);
});
All player methods are synchronous reads or async commands. You never need to mutate internal state directly.
Auth-protected streams
Pass an auth object in the config to add a Bearer token to every HLS manifest and segment request:
const player = nmplayer('player').setup({
playlist: [{ id: '1', url: 'https://protected.cdn.your-domain.com/stream.m3u8' }],
auth: {
bearerToken: () => myAuth.getAccessToken(),
refreshOnUnauthenticated: async () => {
await myAuth.refresh();
},
},
});
player.on('ready', () => {
player.item(0, { autoplay: true });
});
On a 401, refreshOnUnauthenticated fires once and the failed request is retried. A 403 propagates without a retry.
Cleanup
Call dispose() when you remove the player from the page. It tears down the backend, releases the HLS instance, and cleans up all event listeners:
player.dispose();
If you are building a single-page app without a framework, wire this to whatever signals the player is leaving the view, for example a route change callback or a MutationObserver watching the container element:
window.addEventListener('beforeunload', () => {
player.dispose();
});
What to read next
- Quick Start: the first working player in under a minute
- Configuration: every config option and default
- API Methods: full method reference
- Events: all events and their payload shapes
- Plugins: Desktop UI: the built-in controls overlay
- Plugin Development: writing your own plugins