Skip to content

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.

HTML
<!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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
window.addEventListener('beforeunload', () => {
player.dispose();
});