mekovid
v1.1.0
Published
Modern, hafif ve özelleştirilebilir HTML5 video player - HLS, altyazı ve kalite seçimi desteği
Maintainers
Readme
MekoVid — Modern HTML5 Video Player
A modern, lightweight and customizable HTML5 video player. Packed with HLS streaming, automatic subtitle format conversion, quality selection, dub switching, theme system, keyboard/touch support, PiP, AirPlay, Chromecast, and much more.
🌐 Türkçe dokümantasyon için README_TR.md
Table of Contents
- Features
- Installation
- Quick Start
- Usage Examples
- Subtitle Support
- Quality Selection
- Dub / Audio Switching
- Theme System
- Chapter Markers
- Feature System
- Keyboard Shortcuts
- Touch Support
- PiP (Picture-in-Picture)
- AirPlay & Chromecast
- Skip Intro
- localStorage Preferences
- Button Control
- API Reference
- CSS Customization
- Build System
- Project Structure
- Changelog
Features
Playback & Streaming
- 🎬 HLS Streaming — HLS.js integration with live streaming and adaptive bitrate support
- 📺 Quality Selection — HLS auto quality + normal video
sizeattribute support - 🎭 Dub / Audio Switching — Multi audio track management via
data-dubsattribute
Subtitles
- 📝 Subtitle Support — SRT, ASS/SSA and VTT formats; automatic conversion
- 🎨 Subtitle Customization — Size, weight, color, background, offset, outline
- 📱 Mobile Subtitles — Auto scaling based on viewport size (
clamp()+ ResizeObserver)
UI & Themes
- 🎨 Theme System —
default,bar,blue,blue-barthemes; switchable at runtime - 📊 Chapter Markers — Colored segment blocks and dividers on the progress bar
- 🖱️ Modern UI — Progress bar hover tooltip (with chapter name), volume slider fill, stack-based menu navigation
Controls
- ⌨️ Keyboard Shortcuts — Space, arrows, F, M, 0–9 keys
- 👆 Touch Support — Double-tap ±10s skip, swipe volume control, horizontal swipe seek
- 📺 PiP — Picture-in-Picture; works in all modern desktop browsers
- 📡 AirPlay — Auto-detected and enabled in Safari
- 📡 Chromecast / Miracast — Remote Playback API; auto-visible in supported browsers
- ⏭️ Skip Intro — Skip button via
data-skip-intro-start/end
Personalization
- 💾 localStorage Preferences — Volume, mute state and subtitle language saved automatically
- 🔒 Feature System — Every feature can be disabled via HTML attribute or JS options
- 🎛️ Button Control — Show/hide individual control buttons via HTML or JS API
Developer
- ⚡ Lightweight — Minimal footprint, maximum performance
- 🔌 Extended API — Comprehensive programmatic control (
seekTo,setChapters,setTheme, ...)
Installation
via npm
npm install mekovidvia CDN
<!-- CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/mekovid/dist/css/mekovid.min.css">
<!-- JS (Modern) -->
<script type="module" src="https://cdn.jsdelivr.net/npm/mekovid/dist/js/mekovid.min.mjs"></script>
<!-- JS (Legacy browsers) -->
<script src="https://cdn.jsdelivr.net/npm/mekovid/dist/js/mekovid.polyfilled.min.js"></script>Quick Start
HTML
<video id="myVideo" class="mekovid">
<source src="video.mp4" type="video/mp4">
</video>ES Module (npm / Vite / Webpack)
import mekovid from 'mekovid';
import 'mekovid/dist/css/mekovid.min.css';
const player = mekovid('myVideo', {
thumbnail: 'poster.jpg',
autoplay: false
});CDN (Global Script)
<script src="https://cdn.jsdelivr.net/npm/mekovid/dist/js/mekovid.polyfilled.min.js"></script>
<script>
const player = mekovid('myVideo');
player.play();
</script>Usage Examples
Modern Browsers (.mjs)
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="mekovid.min.css">
</head>
<body>
<video id="player" class="mekovid">
<source src="video.mp4" type="video/mp4">
</video>
<script type="module">
import mekovid from './mekovid.min.mjs';
const player = mekovid('player', {
thumbnail: 'poster.jpg',
autoplay: false
});
</script>
</body>
</html>Legacy Browsers (Polyfilled)
<script src="mekovid.polyfilled.min.js"></script>
<script>
var player = mekovid('player', {
thumbnail: 'poster.jpg'
});
</script>HLS Streaming
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script type="module">
import mekovid from './mekovid.min.mjs';
const player = mekovid('player');
player.changeSource('https://example.com/stream.m3u8');
</script>React
import { useEffect, useRef } from 'react';
import mekovid from 'mekovid';
import 'mekovid/dist/css/mekovid.min.css';
function VideoPlayer({ src, poster }) {
const videoRef = useRef(null);
const playerRef = useRef(null);
useEffect(() => {
if (videoRef.current && !playerRef.current) {
playerRef.current = mekovid(videoRef.current.id, {
thumbnail: poster
});
}
return () => {
if (playerRef.current) playerRef.current.destroy();
};
}, []);
return (
<video ref={videoRef} id="react-player" className="mekovid">
<source src={src} type="video/mp4" />
</video>
);
}Vue 3
<template>
<video ref="videoElement" id="vue-player" class="mekovid">
<source :src="src" type="video/mp4">
</video>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import mekovid from 'mekovid';
import 'mekovid/dist/css/mekovid.min.css';
const props = defineProps({ src: String, poster: String });
let player = null;
onMounted(() => {
player = mekovid('vue-player', { thumbnail: props.poster });
});
onUnmounted(() => {
if (player) player.destroy();
});
</script>Svelte
<script>
import { onMount, onDestroy } from 'svelte';
import mekovid from 'mekovid';
import 'mekovid/dist/css/mekovid.min.css';
export let src;
export let poster;
let player;
onMount(() => {
player = mekovid('svelte-player', { thumbnail: poster });
});
onDestroy(() => {
if (player) player.destroy();
});
</script>
<video id="svelte-player" class="mekovid">
<source {src} type="video/mp4">
</video>Lazy Loading
const loadPlayer = async () => {
const { default: mekovid } = await import('mekovid/dist/js/mekovid.min.mjs');
return mekovid('player');
};
document.querySelector('.play-button').addEventListener('click', async () => {
const player = await loadPlayer();
player.play();
});Subtitle Support
MekoVid supports VTT, SRT and ASS/SSA formats. SRT and ASS files are automatically converted to VTT in the browser — no server required.
| Format | Extension | Conversion |
|--------|-----------|------------|
| VTT | .vtt | Used directly |
| SRT | .srt | Auto-converted to VTT |
| ASS/SSA | .ass, .ssa | Auto-converted to VTT |
Subtitles via HTML
<video id="player" class="mekovid" crossorigin="anonymous">
<source src="video.mp4" type="video/mp4">
<!-- VTT (direct) -->
<track kind="subtitles" label="Turkish" srclang="tr" src="tr.vtt" default>
<!-- SRT (auto-convert) -->
<track kind="subtitles" label="English" srclang="en" src="en.srt">
<!-- ASS (auto-convert) -->
<track kind="subtitles" label="Français" srclang="fr" src="fr.ass">
</video>Subtitle Management via API
const player = mekovid('player');
// Add subtitle (format auto-detected)
await player.addSubtitle('subtitle.srt', 'Turkish', 'tr', true);
await player.addSubtitle('subtitle.ass', 'English', 'en');
await player.addSubtitle('subtitle.vtt', 'Français', 'fr');
// Switch active subtitle
player.setActiveSubtitle('tr');
player.setActiveSubtitle('off'); // disable
// Remove all subtitles
player.clearSubtitles();
// Get subtitle list
const subs = player.getSubtitles();
// [{ language: 'tr', label: 'Turkish', mode: 'showing' }, ...]Manual Format Conversion
const { converter } = player;
// Detect format (from content)
const format = converter.detectFormat(content); // 'srt' | 'vtt' | 'ass' | 'unknown'
// Detect format (from URL)
const format = converter.detectFormatFromUrl('subtitle.srt'); // 'srt'
// SRT → VTT
const vtt = converter.srtToVtt(srtContent);
// ASS → VTT
const vtt = converter.assToVtt(assContent);
// Auto-convert
const vtt = converter.toVtt(content);Important Notes
- Subtitle files must be UTF-8 encoded
- Cross-origin subtitles require
crossorigin="anonymous"and CORS headers - Complex ASS styling is not supported — only text is extracted
Quality Selection
Normal Video — Multiple Sources (size attribute)
<video id="player" class="mekovid">
<source src="video-480p.mp4" type="video/mp4" size="480">
<source src="video-720p.mp4" type="video/mp4" size="720">
<source src="video-1080p.mp4" type="video/mp4" size="1080">
<source src="video-2160p.mp4" type="video/mp4" size="2160">
</video>All sources with a size attribute are scanned at initialization and a quality menu is built automatically. Playback position is preserved during quality changes.
| Size | Resolution |
|------|-----------|
| 360 | 360p |
| 480 | 480p SD |
| 720 | 720p HD |
| 1080 | 1080p Full HD |
| 1440 | 1440p 2K |
| 2160 | 2160p 4K |
HLS Video — Auto Quality
<video id="player" class="mekovid">
<source src="video.m3u8" type="application/vnd.apple.mpegurl">
</video>Quality levels are automatically added to the menu when the HLS manifest is parsed. Users can select "Auto" or a manual quality level.
⚠️ Do not use
sizeattribute together with HLS.
Dub / Audio Switching
<video id="player" class="mekovid"
data-dubs='[
{"group":"tr","name":"Turkish","icon":"🇹🇷"},
{"group":"en","name":"English","icon":"🇺🇸"},
{"group":"jp","name":"日本語","icon":"🇯🇵"}
]'
data-current-dub="tr">
<source src="video.mp4" type="video/mp4">
</video>When data-groups is provided ([{ group, name, type, items: [{ link, quality }] }]), selecting a dub switches the source automatically: the dub's HLS entry if there is one, otherwise the MP4 closest to the current quality. During the switch the video is paused, the loading indicator is painted first, the new source seeks to the same second and resumes if it was playing (v1.1).
// Dub change event — fires BEFORE the source changes (v1.1)
videoElement.addEventListener('dubchange', (e) => {
const { group, name, previousGroup, currentTime, wasPlaying } = e.detail;
console.log('New dub:', group, name);
// To handle the source change yourself:
// e.preventDefault();
// player.changeSource(url, { currentTime, autoplay: wasPlaying, reason: 'dub' });
});The active dub comes from data-current-dub; if it is missing, it is inferred from the current source URL.
The menu automatically updates when data-dubs attribute changes (MutationObserver).
Theme System
Four built-in themes are available:
| Theme | Progress Color | Progress Position |
|-------|---------------|-------------------|
| default | Red #ff0000 | Same row as buttons |
| bar | Red #ff0000 | Above buttons (full width) |
| blue | Blue #1a8cff | Same row as buttons |
| blue-bar | Blue #1a8cff | Above buttons (full width) |
<!-- HTML attribute -->
<video data-theme="blue-bar">
<!-- JS options -->
mekovid('player', { theme: 'bar' });
<!-- Switch at runtime -->
player.setTheme('blue');
player.getTheme(); // 'blue'Chapter Markers
Colored segment blocks and chapter name tooltip on the progress bar.
<!-- HTML attribute -->
<video data-chapters='[
{"start": "0:00", "end": "1:38:33", "label": "Chapter 1", "color": "#4fc3f7"},
{"start": "1:38:33", "end": "1:56:43", "label": "Ad Break", "color": "#ff4444"},
{"start": "1:56:43", "end": "3:00:00", "label": "Chapter 2", "color": "#81c784"}
]'>// JS options
mekovid('player', {
chapters: [
{ start: 0, end: 5853, label: 'Chapter 1', color: '#4fc3f7' },
{ start: 5853, end: 6903, label: 'Ad Break', color: '#ff4444' },
{ start: 6903, end: 10800, label: 'Chapter 2', color: '#81c784' }
]
});
// Update at runtime
player.setChapters([...]);
player.getCurrentChapter(); // { start, end, label, color } | null
player.clearChapters();start and end values accept seconds (number) or time strings in "1:38:33" / "01:38:33" format.
Feature System
Every feature can be disabled via HTML attribute or JS options.
<!-- Disable via HTML -->
<video data-disable="pip,airplay,cast">// Via JS options
mekovid('player', {
features: {
pip: true, // PiP button
airplay: true, // AirPlay button (Safari only)
cast: true, // Chromecast button
keyboard: true, // Keyboard shortcuts
touch: true, // Touch support
skipIntro: true, // Skip Intro button
storage: true, // localStorage preferences
}
});
// Toggle at runtime
player.disableFeature('pip');
player.enableFeature('pip');
player.isFeatureEnabled('pip'); // booleanKeyboard Shortcuts
| Key | Action |
|-----|--------|
| Space / K | Play / Pause |
| ← | Seek -10 seconds |
| → | Seek +10 seconds |
| ↑ | Volume +10% |
| ↓ | Volume -10% |
| F | Toggle fullscreen |
| M | Toggle mute |
| 0–9 | Jump to 0%–90% position |
Disable with data-disable="keyboard" or features: { keyboard: false }.
Touch Support
| Gesture | Action | |---------|--------| | Single tap | Show controls; brief wait → play/pause | | Double tap (left) | Seek -10 seconds | | Double tap (right) | Seek +10 seconds | | Horizontal swipe (60px+) | ±10 seconds | | Vertical swipe (left half) | Volume control |
Disable with data-disable="touch" or features: { touch: false }.
PiP (Picture-in-Picture)
A button is automatically added in browsers that support document.pictureInPictureEnabled.
player.enterPiP(); // Promise
player.exitPiP(); // Promise
player.isPiP(); // booleanHide with data-disable="pip" or features: { pip: false }.
AirPlay & Chromecast
AirPlay (Safari only)
A button is created when window.WebKitPlaybackTargetAvailabilityEvent is detected.
Button automatically appears when an AirPlay device is found, stays hidden otherwise.
<!-- Disable -->
<video data-disable="airplay">Chromecast / Miracast (Remote Playback API)
A button is created in browsers that support HTMLVideoElement.prototype.remote (Chrome, Edge, Samsung Browser).
<!-- Disable -->
<video data-disable="cast">Skip Intro
A "Skip Intro ▶" button appears on screen during the specified time range.
<video data-skip-intro-start="82" data-skip-intro-end="172">player.skipIntro(); // Skip programmaticallyDisable with data-disable="skipIntro" or features: { skipIntro: false }.
localStorage Preferences
When enabled, the following values are automatically saved and restored:
| Value | When Saved |
|-------|-----------|
| volume | Volume level changes |
| muted | Mute toggled |
| subtitleLang | Subtitle language changes |
| quality | Quality selected |
player.getPrefs(); // { volume, muted, subtitleLang, quality }
player.clearPrefs(); // Delete all saved preferencesDisable with data-disable="storage" or features: { storage: false }.
Button Control
Show or hide any control bar button via HTML attribute or JS API.
Hide at startup via HTML:
<video data-hide-buttons="fullscreen,settings" ...></video>Hide at startup via JS options:
const player = mekovid('video-id', {
buttons: {
fullscreen: false,
settings: false,
}
});Show/hide at runtime:
player.hideButton('fullscreen') // Hide
player.showButton('fullscreen') // Show
player.toggleButton('fullscreen') // Toggle
player.toggleButton('fullscreen', false) // Force hide
player.isButtonVisible('fullscreen') // boolean
player.getButtons() // [{ name, visible }, ...]Available button names:
| Name | Description |
|------|-------------|
| play | Play/Pause button |
| mute | Mute button |
| volume | Volume slider container |
| time | Time display (00:00 / 00:00) |
| progress | Progress bar container |
| settings | Settings (gear) button |
| fullscreen | Fullscreen button |
| pip | Picture-in-Picture button |
| airplay | AirPlay button |
| cast | Chromecast button |
API Reference
Create Player
const player = mekovid(videoId, options);options:
| Parameter | Type | Description |
|-----------|------|-------------|
| thumbnail | string | Poster/thumbnail URL |
| autoplay | boolean | Auto play (default: false) |
| subtitles | Array | Initial subtitles [{ src, label, language }] |
| defaultSubtitle | string | Default language code |
| theme | string | Theme: 'default' | 'bar' | 'blue' | 'blue-bar' |
| chapters | Array | Chapter markers [{ start, end, label, color }] |
| features | Object | Feature enable/disable settings |
| buttons | Object | Initial button visibility { fullscreen: false, ... } |
| notifications | boolean \| Object | false disables all notifications; per type: { subtitle, subtitleStyle, dub, quality, skipIntro, cast, error, live } (default: true) — v1.1 |
| loader | boolean | false: the built-in loading indicator is not drawn, loadingchange still fires (default: true) — v1.1 |
| bigPlayButton | boolean | false: hides the built-in big play button (default: true) — v1.1 |
| fill | boolean | true or data-fill: the player fills its container's height (e.g. a 16:9 box) — v1.1 |
| subtitle | string | Initial subtitle language ('tr', 'off'...). Starts off if no track has that language; applied to tracks added later too — v1.1 |
| autoSubtitle | boolean | false: without a preference, the first subtitle is not turned on automatically (default: true) — v1.1 |
| subtitleStyle | Object | Initial subtitle style { size, weight, background, color, offset, outline } (no notifications) — v1.1 |
Playback Control
player.play() // Play → Promise
player.pause() // Pause
player.isPlaying() // boolean
player.isPaused() // boolean
player.getCurrentTime() // number (seconds)
player.setCurrentTime(30) // Seek to second
player.seekTo('1:38:33') // Accepts time string OR seconds
player.getDuration() // number (seconds)Volume
player.getVolume() // 0–1
player.setVolume(0.5) // 0–1 range
player.isMuted() // boolean
player.setMuted(true)Fullscreen
player.isFullscreen()
player.enterFullscreen() // Promise
player.exitFullscreen() // Promise
player.toggleFullscreen() // PromisePiP
player.enterPiP() // Promise — enter PiP mode
player.exitPiP() // Promise — exit PiP mode
player.isPiP() // booleanSkip Intro
player.skipIntro() // Jump to data-skip-intro-end timeTheme
player.getTheme() // 'default' | 'bar' | 'blue' | 'blue-bar'
player.setTheme('blue') // Change theme (applied instantly)Chapters
player.setChapters([
{ start: '0:00', end: '1:38:33', label: 'Chapter 1', color: '#4fc3f7' },
{ start: '1:38:33', end: '1:56:43', label: 'Ad Break', color: '#ff4444' },
{ start: '1:56:43', end: '3:00:00', label: 'Chapter 2', color: '#81c784' }
]);
player.getChapters() // Full chapters array
player.getCurrentChapter() // Active chapter object or null
player.clearChapters() // Remove all chaptersFeature Control
player.isFeatureEnabled('pip') // boolean
player.disableFeature('cast') // Disable at runtime
player.enableFeature('cast') // Enable at runtimelocalStorage
player.getPrefs() // { volume, muted, subtitleLang, quality }
player.clearPrefs() // Delete saved preferencesSource / Poster
player.changeSource('new-video.mp4') // Promise — resolves when the source is ready
player.changeSource('stream.m3u8') // HLS auto-detected
player.changeSource('stream.m3u8', { currentTime: 125, autoplay: true, reason: 'source' }) // v1.1
player.setPoster('poster.jpg')
player.getPoster()Subtitles
await player.addSubtitle(src, label, language, isDefault)
player.clearSubtitles()
player.setActiveSubtitle('en') // language code or 'off'; kept as the preference
player.setActiveSubtitle('off', { silent: true }) // without notification (v1.1)
player.getActiveSubtitle() // 'en' | 'off' (v1.1)
player.getSubtitles() // [{ language, label, mode }]
// Subtitle style (v1.1) — values match the settings menu
player.setSubtitleStyle({ size: 40, weight: 'bold', background: 50, color: 'yellow', offset: 20, outline: 2 })
player.setSubtitleStyle({ color: 'cyan' }, { silent: false }) // with notification
player.getSubtitleStyle() // explicitly chosen values: { color: 'cyan', ... }| Key | Values |
|-----|--------|
| size | 20 30 40 50 60 (px) |
| weight | normal bold 900 |
| background | 0 50 75 85 100 (%) |
| color | white yellow cyan lime magenta |
| offset | 0 10 20 30 50 (px, bottom offset) |
| outline | 0 1 2 3 4 (px) |
HLS Info
const info = player.getHLSInfo();
// {
// format: 'HLS',
// url: '...',
// duration: 3600,
// isLive: false,
// levels: [{ index, width, height, bitrate, name }],
// currentLevel: 2,
// currentLevelName: '1080p',
// videoWidth: 1920,
// videoHeight: 1080,
// buffered: [{ start, end }]
// }
player.getHLSInstance() // Access the HLS.js instanceEvents
player.on('play', () => console.log('Playing'));
player.on('pause', () => console.log('Paused'));
player.on('ended', () => console.log('Ended'));
player.on('timeupdate', () => console.log(player.getCurrentTime()));
player.on('dubchange', (e) => console.log(e.detail)); // { group, name, previousGroup, currentTime, wasPlaying } — cancelable
player.on('loadingchange', (e) => console.log(e.detail)); // { loading, reason: 'buffering'|'dub'|'quality'|'source'|null } (v1.1)
player.on('subtitlechange', (e) => console.log(e.detail)); // { language, applied, source: 'user'|'api' } (v1.1)
player.on('subtitlestylechange', (e) => console.log(e.detail)); // { key, value, style, source: 'user'|'api' } (v1.1)
player.off('play', handler);Other
player.getElement() // HTMLVideoElement
player.getContainer() // Wrapper div
player.getVersion() // { player, version, features }
player.destroy() // Completely remove the player
player.getLoadingState() // { loading, reason } (v1.1)
player.isLoading() // boolean (v1.1)
player.showNotification('Message', 'info', 2000) // 'info' | 'success' | 'warning' | 'error'
player.converter // Access the SubtitleConverter instanceCSS Customization
CSS Variables
:root {
--mekovid-primary: #ff0000; /* primary color (progress, highlights) */
--mekovid-primary-light: #FF5028;
--mekovid-bg: #000; /* video background */
--mekovid-controls-bg: rgba(0, 0, 0, 0.3);
--mekovid-menu-bg: rgba(28, 28, 28, 0.9);
--mekovid-text: #fff;
--mekovid-text-muted: rgba(255, 255, 255, 0.7);
--mekovid-border: rgba(255, 255, 255, 0.2);
--mekovid-border-radius: 16px;
--mekovid-font: 'Helvetica Neue 55', Helvetica, Arial, sans-serif;
--mekovid-transition: 0.3s ease;
}Subtitle Style
/* General subtitle appearance */
::cue {
font-size: 20px;
font-weight: bold;
color: white;
background-color: rgba(0, 0, 0, 0.85);
text-shadow: 2px 2px 4px rgba(0, 0, 0, 1);
}
/* WebKit — subtitle position */
video::-webkit-media-text-track-container {
bottom: 70px;
}The following customizations can also be made from the player settings menu:
- Font Size: 20–60px
- Font Weight: Normal, Bold, Extra Bold
- Background Opacity: 0–100%
- Font Color: White, Yellow, Cyan, Green, Pink
- Bottom Offset: 0–50px
- Outline Size: 0–4px
Browser Support
| Build | IE11 | Chrome | Firefox | Safari | Edge |
|-------|------|--------|---------|--------|------|
| .mjs | ✗ | 90+ | 88+ | 14+ | 90+ |
| .polyfilled.js | ✓ | 60+ | 55+ | 11+ | 15+ |
Build System
Commands
npm run build # Run all builds
npm run dev # Watch mode (development)
npm run clean # Clean dist/
npm run build:js # JS build only
npm run build:css # CSS build only
npm run build:rollup # Rollup build onlyOutput Files
dist/js/ (generated by Rollup):
| File | Format | Polyfill | Description |
|------|--------|----------|-------------|
| mekovid.cjs.js | CJS | ✗ | CommonJS (Node.js / require()) |
| mekovid.esm.js | ESM | ✗ | ES Module (bundlers) |
| mekovid.mjs | ESM | ✗ | Modern browsers |
| mekovid.min.mjs | ESM | ✗ | Modern (minified) |
| mekovid.js | UMD | ✗ | Universal build |
| mekovid.min.js | UMD | ✗ | Universal (minified) |
| mekovid.polyfilled.mjs | ESM | ✓ | Polyfilled ESM |
| mekovid.polyfilled.min.mjs | ESM | ✓ | Polyfilled ESM (minified) |
dist/css/:
| File | Description |
|------|-------------|
| mekovid.css | Full CSS |
| mekovid.min.css | Minified CSS |
Project Structure
mekovid/
├── src/
│ ├── js/
│ │ ├── core.js ← Main player class and API
│ │ ├── controls.js ← UI HTML generators
│ │ ├── hls.js ← HLS.js integration
│ │ ├── subtitles.js ← SubtitleConverter
│ │ ├── notifications.js ← Notification system
│ │ ├── utils.js ← Utility functions
│ │ └── index.js ← Main export
│ └── css/
│ └── mekovid.scss ← Main stylesheet
├── dist/
│ ├── js/ ← Compiled JS files
│ └── css/ ← Compiled CSS files
├── build/
│ ├── build.js ← Main build script
│ └── rollup-summary.js ← Build summary script
├── examples/
│ └── simple-player.html ← Test page
├── rollup.config.js
├── README.md ← English docs (this file)
├── README_TR.md ← Turkish docs
└── package.jsonChangelog
v1.1.0 (Current)
New:
notifications,loader,bigPlayButton,fill,subtitle,autoSubtitle,subtitleStyleoptionsloadingchange,subtitlechange,subtitlestylechangeevents;dubchangenow fires before the source changes, with time and play state, and is cancelablesetSubtitleStyle(),getSubtitleStyle(),getActiveSubtitle(),getLoadingState(),isLoading()changeSource(src, { currentTime, autoplay, reason }): resumes from the same second, paints the loading indicator first
Fixes:
- Switching dubs loaded the video twice and event listeners saw time 0
- Subtitles moved up twice as far while the control bar was visible; the default bottom offset (20px) is now actually applied
- Changing "Outline" or "Bottom offset" wiped the other subtitle styles
- The
stalledevent left the loading indicator stuck during playback - The active dub was misdetected for sources sharing the same file path (
data-current-dubnow takes precedence) changeSourcecould show a "Video could not be loaded" notification while clearing the source
v1.0.0
Core Features:
- HLS streaming support (HLS.js)
- SRT, ASS/SSA → VTT automatic conversion
- 6 subtitle customization options (size, weight, color, background, offset, outline)
- Multi-quality selection via
sizeattribute for normal videos - Dub / audio switching (
data-dubs,data-groups) - Stack-based settings menu navigation
- Progress bar hover time tooltip (dynamic positioning)
- Volume slider fill gradient
MutationObserverfordata-dubstracking
Control Features:
- Keyboard shortcuts (Space/K, ←→↑↓, F, M, 0-9)
- Touch support (double-tap skip, swipe volume, horizontal swipe)
- PiP (Picture-in-Picture) — automatic button in supported browsers
- AirPlay — auto-detection via
WebKitPlaybackTargetAvailabilityEventin Safari - Chromecast / Miracast — auto-detection via Remote Playback API
- Skip Intro — animated button via
data-skip-intro-start/end
UI & Themes:
- Theme system:
default,bar,blue,blue-bar - Chapter markers — colored segment blocks, divider lines, tooltip integration
- Mobile subtitle optimization — responsive sizing via
clamp()+ ResizeObserver - Mobile control optimization — hide volume slider, large touch targets
- Double-tap visual indicator (animated overlay)
API Extensions:
seekTo(time)— seconds or"1:38:33"formatsetChapters(),getChapters(),getCurrentChapter(),clearChapters()setTheme(),getTheme()enterPiP(),exitPiP(),isPiP()skipIntro()isFeatureEnabled(),disableFeature(),enableFeature()getPrefs(),clearPrefs()hideButton(),showButton(),toggleButton(),isButtonVisible(),getButtons()
Feature & Button System:
data-disableHTML attributeoptions.featuresJS objectdata-hide-buttonsHTML attributeoptions.buttonsJS object- Every feature and button independently controllable
License
MIT License — LICENSE
Author
Meliksah Sarioglu
