VideoPlayer
OrganismCustom HTML5 video player. Quality, subtitle, audio track, and playback rate selection; custom WebVTT subtitle overlay; auto-hiding controls; programmatic API. Keyboard shortcuts: Space/K=play, ←→=±10s, ↑↓=volume, M=mute, F=fullscreen.
Big Buck Bunny
<%- include('modules/ui/VideoPlayer/VideoPlayer', {
src: 'https://placeholdervideo.dev/1920x1080',
poster: 'https://example.com/poster.jpg',
title: 'Big Buck Bunny',
qualities: [
{ label: '1080p', value: 'https://example.com/video-1080p.mp4' },
{ label: '720p', value: 'https://example.com/video-720p.mp4' },
{ label: '480p', value: 'https://example.com/video-480p.mp4' },
],
defaultQuality: 'https://example.com/video-1080p.mp4',
subtitles: [
{ label: 'Türkçe', srclang: 'tr', src: '/subtitles/tr.vtt' },
{ label: 'English', srclang: 'en', src: '/subtitles/en.vtt' },
],
audioTracks: [
{ label: 'Türkçe' },
{ label: 'English' },
],
}) %>
Lecture — Episode 1
<%- include('modules/ui/VideoPlayer/VideoPlayer', {
src: 'https://example.com/lecture.mp4',
title: 'Lecture — Episode 1',
subtitles: [
{ label: 'Türkçe', srclang: 'tr', src: '/subtitles/tr.vtt' },
],
}) %>
<%- include('modules/ui/VideoPlayer/VideoPlayer', {
src: 'https://placeholdervideo.dev/1920x1080',
autoHideControls: false,
}) %>
<%- include('modules/ui/VideoPlayer/VideoPlayer', {
src: 'https://example.com/promo.mp4',
autoPlay: true,
startMuted: true,
loop: true,
poster: 'https://example.com/poster.jpg',
}) %>
<%
var _id = locals.id || ('vp-' + Math.random().toString(36).substr(2, 9));
var _poster = locals.poster || '';
var _title = locals.title || '';
var _autoPlay = locals.autoPlay || false;
var _loop = locals.loop || false;
var _startMuted = locals.startMuted || false;
var _qualities = locals.qualities || [];
var _defaultQuality = locals.defaultQuality || (_qualities.length > 0 ? _qualities[0].value : '');
var _defaultQualityLabel = (function() {
if (!_qualities.length) return 'Auto';
var match = _qualities.filter(function(q) { return q.value === _defaultQuality; })[0];
return match ? match.label : (_qualities[0] ? _qualities[0].label : 'Auto');
})();
var _subtitles = locals.subtitles || [];
var _audioTracks = locals.audioTracks || [];
var _autoHideControls = (locals.autoHideControls !== undefined) ? locals.autoHideControls : true;
var _enableCast = (locals.enableCast !== undefined) ? locals.enableCast : true;
var _className = locals.className || '';
var _src = locals.src || '';
var _sources = Array.isArray(_src)
? _src
: (typeof _src === 'string' ? [{ src: _src }] : [_src]);
var _hasSettings = true; // always show gear (speed is always available)
var _hasSubs = _subtitles.length > 0;
var _hasQualities = _qualities.length > 0;
var _hasAudio = _audioTracks.length > 1;
var _speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
// shared partial context
var _ctx = {
id: _id,
poster: _poster,
title: _title,
autoPlay: _autoPlay,
loop: _loop,
startMuted: _startMuted,
qualities: _qualities,
defaultQuality: _defaultQuality,
defaultQualityLabel: _defaultQualityLabel,
subtitles: _subtitles,
audioTracks: _audioTracks,
autoHideControls: _autoHideControls,
enableCast: _enableCast,
className: _className,
sources: _sources,
hasSubs: _hasSubs,
hasQualities: _hasQualities,
hasAudio: _hasAudio,
speeds: _speeds,
};
%>
<%/* ── Video element ── */%>
<%- include('./partials/_overlays', { ctx: _ctx }) %>
<%/* ── Controls layer ── */%>
<%/* Vignette */%>
<%- include('./partials/_settings-panel', { ctx: _ctx }) %>
<%- include('./partials/_controls', { ctx: _ctx }) %>
<%/* end controls layer */%>
<%/*
──────────────────────────────────────────────────────────────────────────────
JS-event-vs-callback API (EJS) — note for consumers
──────────────────────────────────────────────────────────────────────────────
The React `VideoPlayer.tsx` exposes callback props (`onQualityChange`,
`onAudioTrackChange`, `onCastStateChange`, `onControlsVisibilityChange`).
This EJS twin runs as a vanilla-JS IIFE and instead dispatches DOM
`CustomEvent`s on the player container:
- `vp:qualitychange` → detail: { value }
- `vp:audiotrackchange` → detail: { index }
Subscribe via:
document.getElementById('').addEventListener('vp:qualitychange', ...);
The imperative API is exposed at `window.__vp[]` (togglePlay, seekBy,
toggleMute, setVolume, setSpeed, setQuality, setSubtitle, etc.).
*/%>
<%/* ── Load shared helper scripts (browsers dedupe identical URLs) ── */%>
<%/* ── Per-component initializer (preserves the IIFE pattern) ── */%>