@wurathh/player-core
v1.0.1
Published
A modern, feature-rich vanilla TypeScript video player with HLS, subtitles, theming, and built-in i18n.
Downloads
248
Maintainers
Readme
Features
- Modern custom player UI built with vanilla TypeScript
- ESM, CommonJS, CSS, and declaration builds
- Optional HLS support via
@wurathh/player-core/hls - Single source or multiple quality source playback
- Subtitles with WebVTT support
- Multiple audio tracks
- Chapters and thumbnail preview support
- Keyboard shortcuts and accessible settings navigation
- Fullscreen and Picture-in-Picture support
- Live playback support with live-edge controls
- Built-in i18n with
auto, built-in locales, and custom locales - Locale-aware digit localization through per-locale
numbersmaps - Typed events, exported types, and framework-agnostic API
Installation
# npm
npm install @wurathh/player-core
# pnpm
pnpm add @wurathh/player-core
# yarn
yarn add @wurathh/player-core
# bun
bun add @wurathh/player-coreCDN Usage
jsDelivr
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@wurathh/[email protected]/dist/wplyr.css"
/>
<script type="module">
import { WPlayer } from "https://cdn.jsdelivr.net/npm/@wurathh/[email protected]/dist/index.js";
new WPlayer("#player", {
src: "https://example.com/video.mp4",
poster: "https://example.com/poster.jpg",
});
</script>unpkg
<link
rel="stylesheet"
href="https://unpkg.com/@wurathh/[email protected]/dist/wplyr.css"
/>
<script type="module">
import { WPlayer } from "https://unpkg.com/@wurathh/[email protected]/dist/index.js";
new WPlayer("#player", {
src: "https://example.com/video.mp4",
poster: "https://example.com/poster.jpg",
});
</script>WPlayer şu an global window export etmez; CDN kullanımında type="module" gereklidir.
Quick Start
import { WPlayer } from "@wurathh/player-core";
import "@wurathh/player-core/styles.css";
const player = new WPlayer("#player", {
src: "https://example.com/video.mp4",
poster: "https://example.com/poster.jpg",
aspectRatio: "16/9",
});<div id="player"></div>For HLS sources, use the dedicated subpath:
import { WPlayer } from "@wurathh/player-core/hls";
import "@wurathh/player-core/styles.css";API Reference
Constructor
new WPlayer(container, options)| Argument | Type | Description |
|------|------|-------------|
| container | string \| HTMLElement | Mount target or selector |
| options | WPlayerOptions | Player options |
WPlayerOptions
| Option | Type | Default | Description |
|------|------|---------|-------------|
| src | string \| VideoSource[] | required | Video source URL or quality source list |
| poster | string | - | Poster image URL |
| aspectRatio | string | "16/9" | CSS aspect ratio |
| autoPlay | boolean | false | Start playback automatically |
| loop | boolean | false | Loop playback |
| muted | boolean | false | Start muted |
| subtitles | SubtitleTrack[] | [] | Subtitle track list |
| audioTracks | AudioTrack[] | [] | Audio track list |
| chapters | Chapter[] | [] | Chapter list |
| thumbnailPreview | ThumbnailPreview | - | Sprite metadata for hover preview |
| showPosterDuringPlayback | boolean | false | Keep poster visible while media is playing |
| config | PlayerConfigInput | merged defaults | Player configuration |
Constructor callbacks
WPlayerOptions also accepts event callbacks directly:
| Callback | Type | Description |
|------|------|-------------|
| onReady | () => void | Player is ready |
| onPlay | () => void | Playback started |
| onPause | () => void | Playback paused |
| onEnded | () => void | Playback ended |
| onSourceChange | (source) => void | Source changed |
| onMetadataLoaded | (duration) => void | Metadata loaded |
| onLoadedData | () => void | Media data loaded |
| onSeeked | () => void | Seek completed |
| onRateChange | (rate) => void | Playback rate changed |
| onTimeUpdate | (time) => void | Current time updated |
| onEnterFullscreen | () => void | Entered fullscreen |
| onExitFullscreen | () => void | Exited fullscreen |
| onEnterPip | () => void | Entered Picture-in-Picture |
| onExitPip | () => void | Exited Picture-in-Picture |
| onVolumeChange | (volume, muted) => void | Volume state changed |
| onControlsHidden | () => void | Controls hidden |
| onControlsShown | () => void | Controls shown |
| onCaptionsEnabled | () => void | Captions enabled |
| onCaptionsDisabled | () => void | Captions disabled |
| onSubtitleChange | (language) => void | Active subtitle changed |
| onAudioTrackChange | (trackId) => void | Active audio track changed |
| onQualityChange | (quality) => void | Active quality changed |
| onSpeedChange | (speed) => void | Playback speed changed |
| onError | (error) => void | Playback error occurred |
Usage Examples
Single source
new WPlayer("#player", {
src: "https://example.com/video.mp4",
poster: "https://example.com/poster.jpg",
});Multiple quality sources
new WPlayer("#player", {
src: [
{ src: "/video-2160p.mp4", quality: 2160 },
{ src: "/video-1080p.mp4", quality: 1080 },
{ src: "/video-720p.mp4", quality: 720 },
{ src: "/video-480p.mp4", quality: 480 },
],
config: {
quality: {
defaultQuality: "auto",
options: [2160, 1080, 720, 480],
fallbackQuality: 720,
forceQuality: false,
},
},
});HLS source
import { WPlayer } from "@wurathh/player-core/hls";
new WPlayer("#player", {
src: "https://example.com/master.m3u8",
config: {
quality: {
defaultQuality: "auto",
options: [],
fallbackQuality: 720,
forceQuality: false,
},
fullscreen: {
enabled: true,
fallbackToViewport: true,
iosNative: true,
container: null,
},
},
});@wurathh/player-core does not register the HLS runtime. If you want .m3u8 playback outside native browser support, import from @wurathh/player-core/hls.
Subtitles, chapters, and audio tracks
new WPlayer("#player", {
src: "/video.mp4",
subtitles: [
{ src: "/subs/en.vtt", srcLang: "en", label: "English", default: true },
{ src: "/subs/tr.vtt", srcLang: "tr", label: "Turkce" },
{ src: "/subs/de.vtt", srcLang: "de", label: "Deutsch" },
],
audioTracks: [
{ id: "main", label: "Original", default: true },
{ id: "dub-en", label: "English Dub" },
],
chapters: [
{ id: "intro", title: "Introduction", startTime: 0, endTime: 45 },
{ id: "main", title: "Main Section", startTime: 45, endTime: 240 },
{ id: "end", title: "Ending", startTime: 240, endTime: 300 },
],
});Thumbnail preview
new WPlayer("#player", {
src: "/video.mp4",
thumbnailPreview: {
url: "/thumbs.jpg",
width: 160,
height: 90,
columns: 5,
rows: 5,
},
});Events
const player = new WPlayer("#player", {
src: "/video.mp4",
});
player
.on("ready", () => console.log("ready"))
.on("play", () => console.log("play"))
.on("pause", () => console.log("pause"))
.on("qualityChange", (quality) => console.log("quality", quality))
.on("subtitleChange", (language) => console.log("subtitle", language))
.on("audioTrackChange", (trackId) => console.log("audio", trackId))
.on("error", (error) => console.error(error));Switching source at runtime
const player = new WPlayer("#player", {
src: "/video-1.mp4",
});
player.setSource(
[
{ src: "/video-2-1080.mp4", quality: 1080 },
{ src: "/video-2-720.mp4", quality: 720 },
],
{
poster: "/poster-2.jpg",
subtitles: [{ src: "/subs/fr.vtt", srcLang: "fr", label: "Francais" }],
autoPlay: true,
},
);Configuration
Full configuration example
new WPlayer("#player", {
src: "/video.mp4",
config: {
controls: {
enabled: true,
autohide: true,
autohideDelay: 2500,
hideOnMobile: false,
displayDuration: true,
seekTime: 10,
clickToPlay: true,
doubleClickToFullscreen: true,
visibility: {},
},
keyboard: {
enabled: true,
global: false,
focusedOnly: true,
seekStep: 5,
volumeStep: 0.1,
},
captions: {
enabled: true,
defaultActive: false,
defaultLanguage: "auto",
updateOnLanguageChange: true,
},
quality: {
defaultQuality: "auto",
options: [2160, 1440, 1080, 720, 480, 360],
fallbackQuality: 720,
forceQuality: false,
},
speed: {
defaultSpeed: 1,
options: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
persistSelection: false,
},
persistence: {
key: "wplyr_state",
volume: true,
muted: true,
speed: false,
},
fullscreen: {
enabled: true,
fallbackToViewport: true,
iosNative: false,
container: null,
},
behavior: {
resetOnEnd: false,
preload: "metadata",
playsinline: true,
autopause: true,
disableContextMenu: false,
liveEdgeThreshold: 3,
},
i18n: {
locale: "auto",
numberingSystem: "auto",
translations: {},
customLocales: {},
},
},
});Configuration options
| Option | Type | Default | Description |
|------|------|---------|-------------|
| enabled | boolean | true | Enable custom controls |
| autohide | boolean | true | Auto-hide controls when idle |
| autohideDelay | number | 2500 | Idle delay before controls hide |
| hideOnMobile | boolean | false | Hide parts of the control UI on mobile |
| displayDuration | boolean | true | Show full duration beside current time |
| seekTime | number | 10 | Seek amount for skip actions |
| clickToPlay | boolean | true | Toggle playback when the media area is clicked |
| doubleClickToFullscreen | boolean | true | Toggle fullscreen on double click/tap center |
| visibility | ControlVisibilityConfig | {} | Responsive per-control visibility rules |
| Option | Type | Default | Description |
|------|------|---------|-------------|
| enabled | boolean | true | Enable keyboard shortcuts |
| global | boolean | false | Bind shortcuts on document instead of container |
| focusedOnly | boolean | true | When global is enabled, only react if player is focused |
| seekStep | number | 5 | Left/right key seek amount |
| volumeStep | number | 0.1 | Up/down key volume amount |
| Option | Type | Default | Description |
|------|------|---------|-------------|
| enabled | boolean | true | Enable caption support |
| defaultActive | boolean | false | Start with captions enabled |
| defaultLanguage | string | "auto" | Preferred subtitle language |
| updateOnLanguageChange | boolean | true | Re-resolve subtitle language when browser language changes |
| Option | Type | Default | Description |
|------|------|---------|-------------|
| defaultQuality | number \| "auto" | "auto" | Initial quality |
| options | number[] | [2160, 1440, 1080, 720, 480, 360] | Allowed quality values |
| fallbackQuality | number | 720 | Fallback quality when requested value is unavailable |
| forceQuality | boolean | false | Force player to stay on configured quality |
| Option | Type | Default | Description |
|------|------|---------|-------------|
| defaultSpeed | number | 1 | Initial playback speed |
| options | number[] | [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] | Allowed playback rates |
| persistSelection | boolean | false | Persist chosen playback speed |
| Option | Type | Default | Description |
|------|------|---------|-------------|
| key | string | "wplyr_state" | Storage key for persisted state |
| volume | boolean | true | Persist volume |
| muted | boolean | true | Persist mute state |
| speed | boolean | false | Persist playback speed |
| Option | Type | Default | Description |
|------|------|---------|-------------|
| enabled | boolean | true | Enable fullscreen controls |
| fallbackToViewport | boolean | true | Use viewport fallback when needed |
| iosNative | boolean | false | Allow iOS native fullscreen fallback |
| container | string \| null | null | Custom fullscreen container selector |
| Option | Type | Default | Description |
|------|------|---------|-------------|
| resetOnEnd | boolean | false | Reset playback position when media ends |
| preload | "none" \| "metadata" \| "auto" | "metadata" | Native preload strategy |
| playsinline | boolean | true | Keep playback inline on iOS where possible |
| autopause | boolean | true | Pause when the player leaves viewport |
| disableContextMenu | boolean | false | Disable native right-click menu |
| liveEdgeThreshold | number | 3 | Live-edge threshold in seconds |
| Option | Type | Default | Description |
|------|------|---------|-------------|
| locale | LocaleInput | "auto" | Built-in locale, custom locale, or browser-aware auto locale |
| numberingSystem | "auto" \| string | "auto" | Intl numbering system override |
| translations | Record<string, unknown> | {} | Partial overrides for active locale |
| customLocales | Record<string, Record<string, unknown>> | {} | User-defined locale dictionaries |
Player Methods
| Method | Return Type | Description |
|------|-------------|-------------|
| play() | Promise<void> | Start playback |
| pause() | void | Pause playback |
| togglePlay() | Promise<void> | Toggle play/pause |
| toggleFullscreen() | Promise<void> | Toggle fullscreen |
| togglePip() | Promise<void> | Toggle Picture-in-Picture |
| setSource(source, options?) | this | Swap source and associated media metadata |
| seek(time) | void | Seek to absolute time |
| seekRelative(delta) | void | Seek relative to current position |
| seekToLive() | void | Jump to live edge |
| goLive() | this | Go live and announce live status |
| setVolume(volume) | void | Set volume between 0 and 1 |
| toggleMute() | void | Toggle mute |
| setMuted(muted) | void | Set mute state |
| setPlaybackRate(rate) | void | Set playback speed |
| setQuality(quality) | void | Set quality or null for auto |
| setAudioTrack(trackId) | void | Set active audio track |
| setCaptionsEnabled(enabled) | Promise<void> | Enable or disable captions |
| retry() | void | Retry loading after an error |
| reload() | void | Reload current media |
| clearError() | void | Clear active error state |
| getState() | Readonly<PlayerState> | Get current state snapshot |
| getCurrentTime() | number | Get current time |
| getDuration() | number | Get duration |
| getVolume() | number | Get current volume |
| getPlaybackRate() | number | Get current playback rate |
| getCurrentQuality() | number \| null | Get active quality |
| getAvailableQualities() | readonly number[] | Get available quality list |
| getCurrentSubtitle() | string \| null | Get active subtitle language |
| getAvailableSubtitles() | readonly SubtitleTrack[] | Get subtitle list |
| getCurrentAudioTrack() | string \| null | Get active audio track |
| getAvailableAudioTracks() | readonly AudioTrack[] | Get audio track list |
| getError() | Readonly<MediaErrorInfo> \| null | Get active error |
| isPlaying() | boolean | Whether media is playing |
| isMuted() | boolean | Whether player is muted |
| isFullscreen() | boolean | Whether player is fullscreen |
| isLive() | boolean | Whether source is live |
| isCaptionsEnabled() | boolean | Whether captions are visible |
| captureProfileSnapshot() | PlayerProfileSnapshot | Capture performance and DOM metrics |
| reset() | void | Reset playback state |
| destroy() | void | Destroy player instance |
| dispose() | void | Alias for destroy() |
Keyboard Shortcuts
| Key | Action |
|-----|--------|
| Space | Play / Pause |
| Left Arrow | Seek backward |
| Right Arrow | Seek forward |
| Up Arrow | Volume up |
| Down Arrow | Volume down |
| F | Toggle fullscreen |
| M | Toggle mute |
| C | Toggle captions |
| > | Increase playback speed |
| < | Decrease playback speed |
| 0-9 | Seek to 0%-90% |
Internationalization
Built-in locales
ar, de, en, es, fr, hi, it, ja, ko, nl, pt, ru, tr, zh
Auto locale
new WPlayer("#player", {
src: "/video.mp4",
config: {
i18n: {
locale: "auto",
},
},
});auto resolves from browser language. Examples:
de-DE->detr-TR->trfr-CA->frif you provide a customfrlocale- Unsupported locales fall back to
en
Override built-in translations
new WPlayer("#player", {
src: "/video.mp4",
config: {
i18n: {
locale: "tr",
translations: {
controls: {
play: "Baslat",
pause: "Dur",
settings: "Ayarlar",
},
settings: {
subtitles: "Altyazi",
},
},
},
},
});Custom locale
new WPlayer("#player", {
src: "/video.mp4",
config: {
i18n: {
locale: "fr",
customLocales: {
fr: {
controls: {
play: "Lire",
pause: "Pause",
settings: "Parametres",
},
settings: {
speed: "Vitesse",
subtitles: "Sous-titres",
quality: "Qualite",
},
errors: {
playbackError: "Erreur de lecture",
retry: "Reessayer",
},
numbers: {
"0": "0",
"1": "1",
"2": "2",
"3": "3",
"4": "4",
"5": "5",
"6": "6",
"7": "7",
"8": "8",
"9": "9",
},
},
},
},
},
});Auto locale with custom locales
new WPlayer("#player", {
src: "/video.mp4",
config: {
i18n: {
locale: "auto",
customLocales: {
fr: {
controls: {
play: "Lire",
},
numbers: {
"0": "0",
"1": "1",
"2": "2",
"3": "3",
"4": "4",
"5": "5",
"6": "6",
"7": "7",
"8": "8",
"9": "9",
},
},
},
},
},
});Digit localization
Each locale can define a numbers map for direct digit replacement.
numbers: {
"0": "٠",
"1": "١",
"2": "٢",
"3": "٣",
"4": "٤",
"5": "٥",
"6": "٦",
"7": "٧",
"8": "٨",
"9": "٩",
}This is used for time, percentages, speed labels, quality labels, and other visible numeric UI values.
Theming
Customize the player through CSS variables.
Quick theme example
:root {
--wplyr-clr-primary: #6366f1;
--wplyr-clr-primary-hover: #4f46e5;
--wplyr-progress-filled-bg: #6366f1;
--wplyr-progress-thumb-bg: #6366f1;
--wplyr-spinner-fill: #6366f1;
--wplyr-big-play-bg: #6366f1;
--wplyr-big-play-hover-bg: #4f46e5;
--wplyr-control-active-color: #6366f1;
--wplyr-control-active-bg: rgba(99, 102, 241, 0.15);
--wplyr-menu-item-hover-bg: rgba(255, 255, 255, 0.08);
--wplyr-badge-bg: #6366f1;
--wplyr-focus-color: #6366f1;
--wplyr-container-bg: #000;
--wplyr-container-radius: 12px;
}CSS variables
| Variable | Default | Description |
|----------|---------|-------------|
| --wplyr-aspect-ratio | 16/9 | Default aspect ratio |
| --wplyr-container-bg | #0f0f0f | Player container background |
| --wplyr-container-radius | 16px | Player corner radius |
| --wplyr-font-family | "Inter", -apple-system, ... | Player font stack |
| --wplyr-text-color | #fff | Base text color |
| --wplyr-focus-width | 2px | Focus outline width |
| --wplyr-focus-style | solid | Focus outline style |
| --wplyr-focus-color | var(--wplyr-clr-primary) | Focus outline color |
| --wplyr-focus-offset | 2px | Focus outline offset |
| --wplyr-video-wrap-bg | #000 | Video wrapper background |
| --wplyr-fullscreen-bg | #000 | Fullscreen background |
| --wplyr-fullscreen-radius | 0 | Fullscreen radius |
| Variable | Default | Description |
|----------|---------|-------------|
| --wplyr-controls-bg | linear-gradient(...) | Controls overlay background |
| --wplyr-controls-pad-y | 16px | Vertical controls padding |
| --wplyr-controls-pad-x | 16px | Horizontal controls padding |
| --wplyr-controls-pad-bottom | 8px | Bottom controls padding |
| --wplyr-controls-gap | 12px | Gap between control rows |
| --wplyr-controls-group-gap | 2px | Gap inside control groups |
| --wplyr-control-btn-size | 34px | Control button size |
| --wplyr-control-icon-size | 20px | Control icon size |
| --wplyr-control-icon-color | #fff | Control icon color |
| --wplyr-control-hover-bg | rgba(255, 255, 255, 0.08) | Hover state background |
| --wplyr-control-active-color | #ff4d4d | Active control color |
| --wplyr-control-active-bg | color-mix(...) | Active control background |
| --wplyr-control-radius | 12px | Control button radius |
| --wplyr-time-gap | 4px | Gap in time display |
| --wplyr-time-margin-left | 12px | Left offset for time block |
| --wplyr-chapter-title-color | #fff | Chapter title color |
| --wplyr-live-badge-bg | rgba(255, 74, 74, 0.16) | Live badge background |
| --wplyr-live-badge-color | rgba(255, 255, 255, 0.92) | Live badge text color |
| --wplyr-live-badge-active-bg | rgba(255, 74, 74, 0.22) | Active live badge background |
| Variable | Default | Description |
|----------|---------|-------------|
| --wplyr-progress-container-height | 24px | Clickable progress area height |
| --wplyr-progress-track-height | 5px | Progress track height |
| --wplyr-progress-track-hover-height | 7px | Track height on hover |
| --wplyr-progress-track-bg | rgba(255, 255, 255, 0.15) | Track background |
| --wplyr-progress-buffered-bg | rgba(255, 255, 255, 0.3) | Buffered amount color |
| --wplyr-progress-filled-bg | #ff4d4d | Played amount color |
| --wplyr-progress-thumb-size | 14px | Scrubber size |
| --wplyr-progress-thumb-bg | #ff4d4d | Scrubber color |
| --wplyr-marker-width | 3px | Chapter marker width |
| --wplyr-marker-height | 12px | Chapter marker height |
| --wplyr-marker-color | #fff | Chapter marker color |
| --wplyr-hover-preview-bottom | 32px | Hover preview bottom offset |
| --wplyr-hover-time-bg | rgba(0, 0, 0, 0.7) | Hover time background |
| --wplyr-thumbnail-border | 2px solid ... | Thumbnail preview border |
| Variable | Default | Description |
|----------|---------|-------------|
| --wplyr-volume-slider-width | 90px | Volume slider width |
| --wplyr-volume-slider-height | 44px | Volume slider hit area |
| --wplyr-volume-slider-padding | 0 10px | Volume slider padding |
| --wplyr-volume-track-height | 4px | Volume track height |
| --wplyr-volume-track-bg | rgba(255, 255, 255, 0.15) | Volume track background |
| --wplyr-volume-filled-bg | #fff | Volume fill color |
| --wplyr-volume-thumb-size | 12px | Volume thumb size |
| --wplyr-volume-thumb-bg | #fff | Volume thumb color |
| Variable | Default | Description |
|----------|---------|-------------|
| --wplyr-menu-bottom | 60px | Menu bottom position |
| --wplyr-menu-right | 20px | Menu right position |
| --wplyr-menu-min-w | 220px | Menu min width |
| --wplyr-menu-max-height | calc(100% - 96px) | Menu max height |
| --wplyr-menu-bg | rgba(15, 15, 15, 0.85) | Menu background |
| --wplyr-menu-radius | 16px | Menu radius |
| --wplyr-menu-border | 1px solid rgba(...) | Menu border |
| --wplyr-menu-shadow | 0 8px 32px rgba(...) | Menu shadow |
| --wplyr-menu-pad | 4px | Menu container padding |
| --wplyr-menu-item-min-height | 36px | Menu item height |
| --wplyr-menu-item-pad | 6px 10px | Menu item padding |
| --wplyr-menu-item-radius | 12px | Menu item radius |
| --wplyr-menu-item-hover-bg | rgba(255, 255, 255, 0.08) | Menu item hover background |
| --wplyr-badge-bg | #ff4d4d | Badge background |
| --wplyr-badge-text | #fff | Badge text color |
| Variable | Default | Description |
|----------|---------|-------------|
| --wplyr-subtitle-bottom | 100px | Subtitle offset from bottom |
| --wplyr-subtitle-max-w | 80% | Subtitle max width |
| --wplyr-subtitle-pad | 4px 12px | Subtitle padding |
| --wplyr-subtitle-bg | rgba(0, 0, 0, 0.75) | Subtitle background |
| --wplyr-subtitle-color | #fff | Subtitle text color |
| --wplyr-subtitle-fs | clamp(0.75rem, 1.8vw, 1rem) | Subtitle font size |
| --wplyr-spinner-size | 48px | Loading spinner size |
| --wplyr-spinner-fill | #ff4d4d | Spinner accent color |
| --wplyr-spinner-track | rgba(255, 255, 255, 0.1) | Spinner track color |
| --wplyr-error-bg | rgba(15, 15, 15, 0.85) | Error panel background |
| --wplyr-error-border | 1px solid rgba(...) | Error panel border |
| --wplyr-error-pad | 20px 12px | Error panel padding |
| --wplyr-big-play-bg | #ff4d4d | Big play button background |
| --wplyr-big-play-hover-bg | #d43f3f | Big play hover background |
| --wplyr-big-play-icon-color | #fff | Big play icon color |
TypeScript
All important types are exported:
import type {
AudioTrack,
Chapter,
LocaleCode,
LocaleInput,
MediaErrorInfo,
PlayerConfig,
PlayerConfigInput,
PlayerErrorCode,
PlayerProfileSnapshot,
PlayerSource,
PlayerState,
SubtitleTrack,
ThumbnailPreview,
WPlayerEventMap,
WPlayerEventName,
WPlayerOptions,
} from "@wurathh/player-core";Development
bun install
bun run dev
bun run typecheck
bun run buildLicense
MIT
