@kodezen/player 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,805 @@
1
+ 'use strict';
2
+
3
+ // src/emitter.ts
4
+ var Emitter = class {
5
+ constructor() {
6
+ this.listeners = /* @__PURE__ */ new Map();
7
+ }
8
+ on(name, cb) {
9
+ let set = this.listeners.get(name);
10
+ if (!set) {
11
+ set = /* @__PURE__ */ new Set();
12
+ this.listeners.set(name, set);
13
+ }
14
+ set.add(cb);
15
+ return () => this.off(name, cb);
16
+ }
17
+ off(name, cb) {
18
+ var _a;
19
+ (_a = this.listeners.get(name)) == null ? void 0 : _a.delete(cb);
20
+ }
21
+ emit(name, ...args) {
22
+ const set = this.listeners.get(name);
23
+ if (!set) return;
24
+ for (const cb of [...set]) cb(...args);
25
+ }
26
+ clear() {
27
+ this.listeners.clear();
28
+ }
29
+ };
30
+
31
+ // src/providers/html5.ts
32
+ var HLS_EXTENSION = /\.m3u8($|\?)/i;
33
+ var HLS_MIME = /mpegurl/i;
34
+ function isHls(source) {
35
+ var _a, _b;
36
+ return HLS_MIME.test((_a = source.mime) != null ? _a : "") || HLS_EXTENSION.test((_b = source.url) != null ? _b : "");
37
+ }
38
+ var Html5Provider = class {
39
+ constructor(source) {
40
+ this.source = source;
41
+ this.capabilities = { seekable: true, rate: true, volume: true };
42
+ this.emitter = new Emitter();
43
+ this.disposers = [];
44
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
+ this.hls = null;
46
+ this.video = document.createElement("video");
47
+ this.video.className = "kzp-media";
48
+ this.video.playsInline = true;
49
+ this.video.preload = "metadata";
50
+ this.video.controls = false;
51
+ if (source.poster) this.video.poster = source.poster;
52
+ }
53
+ async mount(container) {
54
+ var _a, _b;
55
+ const src = (_a = this.source.url) != null ? _a : "";
56
+ container.appendChild(this.video);
57
+ const on = (name, handler) => {
58
+ this.video.addEventListener(name, handler);
59
+ this.disposers.push(() => this.video.removeEventListener(name, handler));
60
+ };
61
+ on("loadedmetadata", () => {
62
+ this.emitter.emit("durationchange", this.getDuration());
63
+ this.emitter.emit("ready");
64
+ });
65
+ on("durationchange", () => this.emitter.emit("durationchange", this.getDuration()));
66
+ on("timeupdate", () => this.emitter.emit("timeupdate", this.video.currentTime));
67
+ on("play", () => this.emitter.emit("play"));
68
+ on("pause", () => this.emitter.emit("pause"));
69
+ on("ended", () => this.emitter.emit("ended"));
70
+ if (isHls(this.source) && !this.video.canPlayType("application/vnd.apple.mpegurl")) {
71
+ on("error", () => this.emitter.emit("error", "Media failed to load."));
72
+ const { default: Hls } = await import('hls.js');
73
+ if (!Hls.isSupported()) {
74
+ this.emitter.emit("error", "HLS playback is not supported in this browser.");
75
+ return;
76
+ }
77
+ this.hls = new Hls();
78
+ this.hls.on(Hls.Events.ERROR, (_event, data) => {
79
+ if (data.fatal) this.emitter.emit("error", "Media failed to load.");
80
+ });
81
+ this.hls.loadSource(src);
82
+ this.hls.attachMedia(this.video);
83
+ } else {
84
+ const el = document.createElement("source");
85
+ el.src = src;
86
+ el.type = (_b = this.source.mime) != null ? _b : isHls(this.source) ? "application/vnd.apple.mpegurl" : "video/mp4";
87
+ this.video.appendChild(el);
88
+ on("error", () => this.emitter.emit("error", "Media failed to load."));
89
+ }
90
+ if (this.video.readyState >= 1) {
91
+ this.emitter.emit("durationchange", this.getDuration());
92
+ this.emitter.emit("ready");
93
+ }
94
+ }
95
+ play() {
96
+ void this.video.play().catch(() => {
97
+ });
98
+ }
99
+ pause() {
100
+ this.video.pause();
101
+ }
102
+ seek(seconds) {
103
+ const d = this.getDuration();
104
+ this.video.currentTime = d > 0 ? Math.min(seconds, d) : Math.max(0, seconds);
105
+ }
106
+ getCurrentTime() {
107
+ return this.video.currentTime || 0;
108
+ }
109
+ getDuration() {
110
+ return Number.isFinite(this.video.duration) ? this.video.duration : 0;
111
+ }
112
+ setVolume(v) {
113
+ this.video.volume = Math.min(1, Math.max(0, v));
114
+ }
115
+ getVolume() {
116
+ return this.video.volume;
117
+ }
118
+ setMuted(m) {
119
+ this.video.muted = m;
120
+ }
121
+ isMuted() {
122
+ return this.video.muted;
123
+ }
124
+ setRate(r) {
125
+ this.video.playbackRate = r;
126
+ }
127
+ getRate() {
128
+ return this.video.playbackRate;
129
+ }
130
+ isPlaying() {
131
+ return !this.video.paused && !this.video.ended;
132
+ }
133
+ on(event, cb) {
134
+ return this.emitter.on(event, cb);
135
+ }
136
+ destroy() {
137
+ var _a;
138
+ for (const dispose of this.disposers) dispose();
139
+ this.disposers = [];
140
+ this.emitter.clear();
141
+ (_a = this.hls) == null ? void 0 : _a.destroy();
142
+ this.hls = null;
143
+ this.video.removeAttribute("src");
144
+ this.video.load();
145
+ this.video.remove();
146
+ }
147
+ };
148
+
149
+ // src/providers/youtube.ts
150
+ var apiPromise = null;
151
+ function loadApi() {
152
+ if (typeof window === "undefined") return Promise.reject(new Error("no window"));
153
+ if (window.YT && window.YT.Player) return Promise.resolve(window.YT);
154
+ if (apiPromise) return apiPromise;
155
+ apiPromise = new Promise((resolve) => {
156
+ const prev = window.onYouTubeIframeAPIReady;
157
+ window.onYouTubeIframeAPIReady = () => {
158
+ prev == null ? void 0 : prev();
159
+ resolve(window.YT);
160
+ };
161
+ const tag = document.createElement("script");
162
+ tag.src = "https://www.youtube.com/iframe_api";
163
+ document.head.appendChild(tag);
164
+ });
165
+ return apiPromise;
166
+ }
167
+ function parseId(source) {
168
+ var _a, _b;
169
+ if (source.id) return source.id;
170
+ const url = (_a = source.url) != null ? _a : "";
171
+ const m = url.match(/(?:youtu\.be\/|[?&]v=|\/embed\/|\/shorts\/)([\w-]{11})/);
172
+ return (_b = m == null ? void 0 : m[1]) != null ? _b : url;
173
+ }
174
+ var YouTubeProvider = class {
175
+ constructor(source) {
176
+ this.source = source;
177
+ this.capabilities = { seekable: true, rate: true, volume: true };
178
+ this.emitter = new Emitter();
179
+ this.yt = null;
180
+ this.current = 0;
181
+ this.duration = 0;
182
+ this.rate = 1;
183
+ this.muted = false;
184
+ this.volume = 1;
185
+ this.playing = false;
186
+ this.ticker = null;
187
+ }
188
+ async mount(container) {
189
+ const host = document.createElement("div");
190
+ host.className = "kzp-media kzp-embed";
191
+ container.appendChild(host);
192
+ this.host = host;
193
+ const YT = await loadApi();
194
+ this.yt = new YT.Player(host, {
195
+ videoId: parseId(this.source),
196
+ playerVars: {
197
+ controls: 0,
198
+ modestbranding: 1,
199
+ rel: 0,
200
+ playsinline: 1,
201
+ disablekb: 1,
202
+ iv_load_policy: 3
203
+ },
204
+ events: {
205
+ onReady: () => {
206
+ var _a, _b, _c;
207
+ this.duration = this.yt.getDuration() || 0;
208
+ this.volume = ((_c = (_b = (_a = this.yt).getVolume) == null ? void 0 : _b.call(_a)) != null ? _c : 100) / 100;
209
+ this.emitter.emit("durationchange", this.duration);
210
+ this.emitter.emit("ready");
211
+ },
212
+ onStateChange: (e) => {
213
+ const S = YT.PlayerState;
214
+ if (e.data === S.PLAYING) {
215
+ this.playing = true;
216
+ this.duration = this.yt.getDuration() || this.duration;
217
+ this.emitter.emit("durationchange", this.duration);
218
+ this.emitter.emit("play");
219
+ this.startTicker();
220
+ } else if (e.data === S.PAUSED) {
221
+ this.playing = false;
222
+ this.emitter.emit("pause");
223
+ this.stopTicker();
224
+ } else if (e.data === S.ENDED) {
225
+ this.playing = false;
226
+ this.emitter.emit("ended");
227
+ this.stopTicker();
228
+ }
229
+ },
230
+ onError: () => this.emitter.emit("error", "YouTube playback error.")
231
+ }
232
+ });
233
+ }
234
+ startTicker() {
235
+ this.stopTicker();
236
+ this.ticker = window.setInterval(() => {
237
+ if (!this.yt) return;
238
+ this.current = this.yt.getCurrentTime() || 0;
239
+ this.emitter.emit("timeupdate", this.current);
240
+ }, 250);
241
+ }
242
+ stopTicker() {
243
+ if (this.ticker !== null) {
244
+ clearInterval(this.ticker);
245
+ this.ticker = null;
246
+ }
247
+ }
248
+ play() {
249
+ var _a, _b;
250
+ (_b = (_a = this.yt) == null ? void 0 : _a.playVideo) == null ? void 0 : _b.call(_a);
251
+ }
252
+ pause() {
253
+ var _a, _b;
254
+ (_b = (_a = this.yt) == null ? void 0 : _a.pauseVideo) == null ? void 0 : _b.call(_a);
255
+ }
256
+ seek(seconds) {
257
+ var _a, _b;
258
+ const s = Math.max(0, seconds);
259
+ (_b = (_a = this.yt) == null ? void 0 : _a.seekTo) == null ? void 0 : _b.call(_a, s, true);
260
+ this.current = s;
261
+ }
262
+ getCurrentTime() {
263
+ return this.current;
264
+ }
265
+ getDuration() {
266
+ return this.duration;
267
+ }
268
+ setVolume(v) {
269
+ var _a, _b;
270
+ this.volume = Math.min(1, Math.max(0, v));
271
+ (_b = (_a = this.yt) == null ? void 0 : _a.setVolume) == null ? void 0 : _b.call(_a, this.volume * 100);
272
+ }
273
+ getVolume() {
274
+ return this.volume;
275
+ }
276
+ setMuted(m) {
277
+ var _a, _b, _c, _d;
278
+ this.muted = m;
279
+ if (m) (_b = (_a = this.yt) == null ? void 0 : _a.mute) == null ? void 0 : _b.call(_a);
280
+ else (_d = (_c = this.yt) == null ? void 0 : _c.unMute) == null ? void 0 : _d.call(_c);
281
+ }
282
+ isMuted() {
283
+ return this.muted;
284
+ }
285
+ setRate(r) {
286
+ var _a, _b;
287
+ this.rate = r;
288
+ (_b = (_a = this.yt) == null ? void 0 : _a.setPlaybackRate) == null ? void 0 : _b.call(_a, r);
289
+ }
290
+ getRate() {
291
+ return this.rate;
292
+ }
293
+ isPlaying() {
294
+ return this.playing;
295
+ }
296
+ on(event, cb) {
297
+ return this.emitter.on(event, cb);
298
+ }
299
+ destroy() {
300
+ var _a, _b, _c;
301
+ this.stopTicker();
302
+ this.emitter.clear();
303
+ (_b = (_a = this.yt) == null ? void 0 : _a.destroy) == null ? void 0 : _b.call(_a);
304
+ (_c = this.host) == null ? void 0 : _c.remove();
305
+ }
306
+ };
307
+
308
+ // src/providers/vimeo.ts
309
+ var apiPromise2 = null;
310
+ function loadApi2() {
311
+ if (typeof window === "undefined") return Promise.reject(new Error("no window"));
312
+ if (window.Vimeo && window.Vimeo.Player) return Promise.resolve(window.Vimeo);
313
+ if (apiPromise2) return apiPromise2;
314
+ apiPromise2 = new Promise((resolve, reject) => {
315
+ const tag = document.createElement("script");
316
+ tag.src = "https://player.vimeo.com/api/player.js";
317
+ tag.onload = () => resolve(window.Vimeo);
318
+ tag.onerror = () => reject(new Error("Vimeo API failed to load"));
319
+ document.head.appendChild(tag);
320
+ });
321
+ return apiPromise2;
322
+ }
323
+ function parseSource(source) {
324
+ var _a;
325
+ if (source.id) return { id: Number(source.id) };
326
+ const url = (_a = source.url) != null ? _a : "";
327
+ const m = url.match(/vimeo\.com\/(?:video\/)?(\d+)/);
328
+ if (m == null ? void 0 : m[1]) return { id: Number(m[1]) };
329
+ return { url };
330
+ }
331
+ var VimeoProvider = class {
332
+ constructor(source) {
333
+ this.source = source;
334
+ this.capabilities = { seekable: true, rate: true, volume: true };
335
+ this.emitter = new Emitter();
336
+ this.player = null;
337
+ this.current = 0;
338
+ this.duration = 0;
339
+ this.rate = 1;
340
+ this.muted = false;
341
+ this.volume = 1;
342
+ this.playing = false;
343
+ }
344
+ async mount(container) {
345
+ const host = document.createElement("div");
346
+ host.className = "kzp-media kzp-embed";
347
+ container.appendChild(host);
348
+ this.host = host;
349
+ const Vimeo = await loadApi2();
350
+ const opts = { controls: false, responsive: false, ...parseSource(this.source) };
351
+ this.player = new Vimeo.Player(host, opts);
352
+ this.player.on("timeupdate", (d) => {
353
+ var _a;
354
+ this.current = (_a = d.seconds) != null ? _a : this.current;
355
+ if (d.duration) this.duration = d.duration;
356
+ this.emitter.emit("timeupdate", this.current);
357
+ });
358
+ this.player.on("play", () => {
359
+ this.playing = true;
360
+ this.emitter.emit("play");
361
+ });
362
+ this.player.on("pause", () => {
363
+ this.playing = false;
364
+ this.emitter.emit("pause");
365
+ });
366
+ this.player.on("ended", () => {
367
+ this.playing = false;
368
+ this.emitter.emit("ended");
369
+ });
370
+ this.player.on("error", () => this.emitter.emit("error", "Vimeo playback error."));
371
+ try {
372
+ this.duration = await this.player.getDuration() || 0;
373
+ this.emitter.emit("durationchange", this.duration);
374
+ } catch {
375
+ }
376
+ this.emitter.emit("ready");
377
+ }
378
+ play() {
379
+ var _a, _b;
380
+ void ((_b = (_a = this.player) == null ? void 0 : _a.play) == null ? void 0 : _b.call(_a));
381
+ }
382
+ pause() {
383
+ var _a, _b;
384
+ void ((_b = (_a = this.player) == null ? void 0 : _a.pause) == null ? void 0 : _b.call(_a));
385
+ }
386
+ seek(seconds) {
387
+ var _a, _b;
388
+ this.current = Math.max(0, seconds);
389
+ void ((_b = (_a = this.player) == null ? void 0 : _a.setCurrentTime) == null ? void 0 : _b.call(_a, this.current));
390
+ }
391
+ getCurrentTime() {
392
+ return this.current;
393
+ }
394
+ getDuration() {
395
+ return this.duration;
396
+ }
397
+ setVolume(v) {
398
+ var _a, _b;
399
+ this.volume = Math.min(1, Math.max(0, v));
400
+ void ((_b = (_a = this.player) == null ? void 0 : _a.setVolume) == null ? void 0 : _b.call(_a, this.volume));
401
+ }
402
+ getVolume() {
403
+ return this.volume;
404
+ }
405
+ setMuted(m) {
406
+ var _a, _b;
407
+ this.muted = m;
408
+ void ((_b = (_a = this.player) == null ? void 0 : _a.setVolume) == null ? void 0 : _b.call(_a, m ? 0 : this.volume));
409
+ }
410
+ isMuted() {
411
+ return this.muted;
412
+ }
413
+ setRate(r) {
414
+ var _a, _b;
415
+ this.rate = r;
416
+ void ((_b = (_a = this.player) == null ? void 0 : _a.setPlaybackRate) == null ? void 0 : _b.call(_a, r));
417
+ }
418
+ getRate() {
419
+ return this.rate;
420
+ }
421
+ isPlaying() {
422
+ return this.playing;
423
+ }
424
+ on(event, cb) {
425
+ return this.emitter.on(event, cb);
426
+ }
427
+ destroy() {
428
+ var _a, _b, _c;
429
+ this.emitter.clear();
430
+ void ((_b = (_a = this.player) == null ? void 0 : _a.destroy) == null ? void 0 : _b.call(_a));
431
+ (_c = this.host) == null ? void 0 : _c.remove();
432
+ }
433
+ };
434
+
435
+ // src/providers/index.ts
436
+ function createProvider(source) {
437
+ switch (source.type) {
438
+ case "html5":
439
+ case "external":
440
+ return new Html5Provider(source);
441
+ case "youtube":
442
+ return new YouTubeProvider(source);
443
+ case "vimeo":
444
+ return new VimeoProvider(source);
445
+ default:
446
+ throw new Error(`[kodezen-player] Unknown source type "${String(source.type)}".`);
447
+ }
448
+ }
449
+
450
+ // src/core/Controls.ts
451
+ var ICONS = {
452
+ play: '<path d="M8 5v14l11-7z"/>',
453
+ pause: '<path d="M6 5h4v14H6zM14 5h4v14h-4z"/>',
454
+ volume: '<path d="M3 10v4h4l5 5V5L7 10H3zm13.5 2a4.5 4.5 0 0 0-2.5-4v8a4.5 4.5 0 0 0 2.5-4z"/>',
455
+ muted: '<path d="M3 10v4h4l5 5V5L7 10H3zm13 2 3 3 1-1-3-3 3-3-1-1-3 3-3-3-1 1 3 3-3 3 1 1z"/>',
456
+ full: '<path d="M4 4h6v2H6v4H4V4zm10 0h6v6h-2V6h-4V4zM4 14h2v4h4v2H4v-6zm14 0h2v6h-6v-2h4v-4z"/>'
457
+ };
458
+ function icon(paths) {
459
+ return `<svg viewBox="0 0 24 24" aria-hidden="true">${paths}</svg>`;
460
+ }
461
+ function fmt(sec) {
462
+ if (!Number.isFinite(sec) || sec < 0) sec = 0;
463
+ const s = Math.floor(sec % 60);
464
+ const m = Math.floor(sec / 60 % 60);
465
+ const h = Math.floor(sec / 3600);
466
+ const mm = h > 0 ? String(m).padStart(2, "0") : String(m);
467
+ const ss = String(s).padStart(2, "0");
468
+ return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
469
+ }
470
+ var DEFAULT_CONFIG = {
471
+ playPause: true,
472
+ seek: true,
473
+ time: true,
474
+ volume: true,
475
+ speed: true,
476
+ fullscreen: true
477
+ };
478
+ var Controls = class {
479
+ constructor(cb, opts) {
480
+ this.cb = cb;
481
+ this.scrubbing = false;
482
+ this.seekableMax = Infinity;
483
+ this.config = { ...DEFAULT_CONFIG, ...opts.config };
484
+ this.lockSeek = opts.lockSeek;
485
+ this.el = document.createElement("div");
486
+ this.el.className = "kzp-controls";
487
+ this.build(opts.speeds);
488
+ }
489
+ build(speeds) {
490
+ if (this.config.playPause) {
491
+ this.playBtn = this.button("kzp-play", "Play", ICONS.play);
492
+ this.playBtn.addEventListener("click", () => this.cb.onToggle());
493
+ this.el.appendChild(this.playBtn);
494
+ }
495
+ if (this.config.seek) {
496
+ this.scrubber = document.createElement("input");
497
+ this.scrubber.type = "range";
498
+ this.scrubber.className = "kzp-scrubber";
499
+ this.scrubber.min = "0";
500
+ this.scrubber.max = "0";
501
+ this.scrubber.step = "0.1";
502
+ this.scrubber.value = "0";
503
+ this.scrubber.setAttribute("aria-label", "Seek");
504
+ const commit = () => this.cb.onSeek(this.clamp(Number(this.scrubber.value)));
505
+ this.scrubber.addEventListener("pointerdown", (e) => {
506
+ if (this.lockSeek) {
507
+ e.preventDefault();
508
+ return;
509
+ }
510
+ this.scrubbing = true;
511
+ });
512
+ this.scrubber.addEventListener("keydown", (e) => {
513
+ if (this.lockSeek) e.preventDefault();
514
+ });
515
+ this.scrubber.addEventListener("input", commit);
516
+ this.scrubber.addEventListener("change", () => {
517
+ commit();
518
+ this.scrubbing = false;
519
+ });
520
+ this.el.appendChild(this.scrubber);
521
+ }
522
+ if (this.config.time) {
523
+ this.timeEl = document.createElement("span");
524
+ this.timeEl.className = "kzp-time";
525
+ this.timeEl.textContent = "0:00 / 0:00";
526
+ this.el.appendChild(this.timeEl);
527
+ }
528
+ if (this.config.volume) {
529
+ const wrap = document.createElement("div");
530
+ wrap.className = "kzp-volume";
531
+ this.volBtn = this.button("kzp-mute", "Mute", ICONS.volume);
532
+ this.volBtn.addEventListener("click", () => this.cb.onMuteToggle());
533
+ this.volRange = document.createElement("input");
534
+ this.volRange.type = "range";
535
+ this.volRange.className = "kzp-vol-range";
536
+ this.volRange.min = "0";
537
+ this.volRange.max = "1";
538
+ this.volRange.step = "0.05";
539
+ this.volRange.value = "1";
540
+ this.volRange.setAttribute("aria-label", "Volume");
541
+ this.volRange.addEventListener("input", () => this.cb.onVolume(Number(this.volRange.value)));
542
+ wrap.append(this.volBtn, this.volRange);
543
+ this.el.appendChild(wrap);
544
+ }
545
+ if (this.config.speed && speeds.length > 1) {
546
+ const wrap = document.createElement("div");
547
+ wrap.className = "kzp-speed";
548
+ this.speedBtn = this.button("kzp-speed-btn", "Playback speed", "");
549
+ this.speedBtn.textContent = "1x";
550
+ this.speedMenu = document.createElement("div");
551
+ this.speedMenu.className = "kzp-speed-menu";
552
+ this.speedMenu.hidden = true;
553
+ for (const rate of speeds) {
554
+ const item = document.createElement("button");
555
+ item.type = "button";
556
+ item.textContent = `${rate}x`;
557
+ item.dataset.rate = String(rate);
558
+ item.addEventListener("click", () => {
559
+ this.cb.onRate(rate);
560
+ this.speedMenu.hidden = true;
561
+ });
562
+ this.speedMenu.appendChild(item);
563
+ }
564
+ this.speedBtn.addEventListener("click", () => {
565
+ this.speedMenu.hidden = !this.speedMenu.hidden;
566
+ });
567
+ wrap.append(this.speedBtn, this.speedMenu);
568
+ this.el.appendChild(wrap);
569
+ }
570
+ if (this.config.fullscreen) {
571
+ const btn = this.button("kzp-full", "Fullscreen", ICONS.full);
572
+ btn.addEventListener("click", () => this.cb.onFullscreen());
573
+ this.el.appendChild(btn);
574
+ }
575
+ }
576
+ button(cls, label, svg) {
577
+ const b = document.createElement("button");
578
+ b.type = "button";
579
+ b.className = cls;
580
+ b.setAttribute("aria-label", label);
581
+ if (svg) b.innerHTML = icon(svg);
582
+ return b;
583
+ }
584
+ clamp(value) {
585
+ return this.lockSeek ? Math.min(value, this.seekableMax) : value;
586
+ }
587
+ setLockSeek(locked) {
588
+ this.lockSeek = locked;
589
+ }
590
+ /** Push the latest state into the DOM. Called on every player timeupdate. */
591
+ update(state, extras) {
592
+ if (this.playBtn) {
593
+ this.playBtn.innerHTML = icon(state.playing ? ICONS.pause : ICONS.play);
594
+ this.playBtn.setAttribute("aria-label", state.playing ? "Pause" : "Play");
595
+ }
596
+ if (this.scrubber) {
597
+ this.seekableMax = this.lockSeek ? state.furthest : Infinity;
598
+ this.scrubber.max = String(state.duration || 0);
599
+ if (!this.scrubbing) this.scrubber.value = String(state.current);
600
+ }
601
+ if (this.timeEl) {
602
+ this.timeEl.textContent = `${fmt(state.current)} / ${fmt(state.duration)}`;
603
+ }
604
+ if (this.volBtn) {
605
+ this.volBtn.innerHTML = icon(extras.muted || extras.volume === 0 ? ICONS.muted : ICONS.volume);
606
+ }
607
+ if (this.volRange && document.activeElement !== this.volRange) {
608
+ this.volRange.value = String(extras.muted ? 0 : extras.volume);
609
+ }
610
+ if (this.speedBtn) this.speedBtn.textContent = `${extras.rate}x`;
611
+ }
612
+ destroy() {
613
+ this.el.remove();
614
+ }
615
+ };
616
+
617
+ // src/core/Player.ts
618
+ var DEFAULT_SPEEDS = [0.5, 1, 1.25, 1.5, 2];
619
+ var NATURAL_ADVANCE_MAX = 2;
620
+ var Player = class {
621
+ constructor(container, options) {
622
+ this.container = container;
623
+ this.options = options;
624
+ this.emitter = new Emitter();
625
+ this.furthest = 0;
626
+ this.lastTick = 0;
627
+ this.duration = 0;
628
+ this.playing = false;
629
+ this.ready = false;
630
+ this.disposers = [];
631
+ var _a, _b, _c, _d, _e, _f, _g;
632
+ const speeds = (_a = options.speeds) != null ? _a : DEFAULT_SPEEDS;
633
+ this.root = document.createElement("div");
634
+ this.root.className = "kzp" + (options.className ? ` ${options.className}` : "");
635
+ if (options.lockSeek) this.root.classList.add("kzp--locked");
636
+ this.stage = document.createElement("div");
637
+ this.stage.className = "kzp-stage";
638
+ this.root.appendChild(this.stage);
639
+ const hitLayer = document.createElement("div");
640
+ hitLayer.className = "kzp-hit-layer";
641
+ hitLayer.addEventListener("click", () => this.toggle());
642
+ this.stage.appendChild(hitLayer);
643
+ this.provider = createProvider(options.source);
644
+ const caps = this.provider.capabilities;
645
+ this.controls = new Controls(
646
+ {
647
+ onToggle: () => this.toggle(),
648
+ onSeek: (s) => this.seek(s),
649
+ onVolume: (v) => {
650
+ this.provider.setMuted(false);
651
+ this.provider.setVolume(v);
652
+ this.render();
653
+ },
654
+ onMuteToggle: () => {
655
+ this.provider.setMuted(!this.provider.isMuted());
656
+ this.render();
657
+ },
658
+ onRate: (r) => {
659
+ this.provider.setRate(r);
660
+ this.render();
661
+ },
662
+ onFullscreen: () => this.toggleFullscreen()
663
+ },
664
+ {
665
+ config: {
666
+ ...options.controls,
667
+ seek: ((_c = (_b = options.controls) == null ? void 0 : _b.seek) != null ? _c : true) && caps.seekable,
668
+ speed: ((_e = (_d = options.controls) == null ? void 0 : _d.speed) != null ? _e : true) && caps.rate,
669
+ volume: ((_g = (_f = options.controls) == null ? void 0 : _f.volume) != null ? _g : true) && caps.volume
670
+ },
671
+ speeds,
672
+ lockSeek: !!options.lockSeek
673
+ }
674
+ );
675
+ this.root.appendChild(this.controls.el);
676
+ this.container.appendChild(this.root);
677
+ this.wireProvider();
678
+ void this.provider.mount(this.stage);
679
+ }
680
+ wireProvider() {
681
+ const p = this.provider;
682
+ this.disposers.push(
683
+ p.on("ready", () => this.onReady()),
684
+ p.on("durationchange", (d) => {
685
+ this.duration = d;
686
+ this.emitter.emit("durationchange", d);
687
+ this.render();
688
+ }),
689
+ p.on("timeupdate", (current) => this.onTimeUpdate(current)),
690
+ p.on("play", () => {
691
+ this.playing = true;
692
+ this.emitter.emit("play");
693
+ this.render();
694
+ }),
695
+ p.on("pause", () => {
696
+ this.playing = false;
697
+ this.emitter.emit("pause");
698
+ this.render();
699
+ }),
700
+ p.on("ended", () => {
701
+ this.playing = false;
702
+ this.emitter.emit("ended");
703
+ this.render();
704
+ }),
705
+ p.on("error", (msg) => this.emitter.emit("error", msg))
706
+ );
707
+ }
708
+ onReady() {
709
+ var _a;
710
+ if (this.ready) return;
711
+ this.ready = true;
712
+ this.duration = this.provider.getDuration();
713
+ if (this.options.muted) this.provider.setMuted(true);
714
+ const resume = (_a = this.options.resumeAt) != null ? _a : 0;
715
+ if (resume > 0 && (this.duration === 0 || resume < this.duration - 2)) {
716
+ this.provider.seek(resume);
717
+ this.furthest = resume;
718
+ this.lastTick = resume;
719
+ }
720
+ this.emitter.emit("ready", { duration: this.duration });
721
+ if (this.options.autoplay) this.provider.play();
722
+ this.render();
723
+ }
724
+ onTimeUpdate(current) {
725
+ const advance = current - this.lastTick;
726
+ if (advance > 0 && advance <= NATURAL_ADVANCE_MAX && current > this.furthest) {
727
+ this.furthest = current;
728
+ }
729
+ this.lastTick = current;
730
+ this.emitter.emit("timeupdate", this.snapshot());
731
+ this.render();
732
+ }
733
+ snapshot() {
734
+ const current = this.provider.getCurrentTime();
735
+ const duration = this.duration || this.provider.getDuration();
736
+ const percent = duration > 0 ? Math.min(100, Math.round(this.furthest / duration * 100)) : 0;
737
+ return { current, furthest: this.furthest, duration, percent, playing: this.playing };
738
+ }
739
+ render() {
740
+ this.controls.update(this.snapshot(), {
741
+ muted: this.provider.isMuted(),
742
+ volume: this.provider.getVolume(),
743
+ rate: this.provider.getRate()
744
+ });
745
+ }
746
+ toggleFullscreen() {
747
+ var _a, _b, _c;
748
+ if (document.fullscreenElement === this.root) {
749
+ void ((_a = document.exitFullscreen) == null ? void 0 : _a.call(document));
750
+ } else {
751
+ void ((_c = (_b = this.root).requestFullscreen) == null ? void 0 : _c.call(_b));
752
+ }
753
+ }
754
+ // ---- Public API ----
755
+ play() {
756
+ this.provider.play();
757
+ }
758
+ pause() {
759
+ this.provider.pause();
760
+ }
761
+ toggle() {
762
+ if (this.provider.isPlaying()) this.provider.pause();
763
+ else this.provider.play();
764
+ }
765
+ seek(seconds) {
766
+ if (!this.provider.capabilities.seekable) return;
767
+ const target = this.options.lockSeek && seconds > this.furthest ? this.furthest : Math.max(0, seconds);
768
+ this.provider.seek(target);
769
+ this.lastTick = target;
770
+ this.render();
771
+ }
772
+ setLockSeek(locked) {
773
+ this.options.lockSeek = locked;
774
+ this.controls.setLockSeek(locked);
775
+ this.root.classList.toggle("kzp--locked", locked);
776
+ this.render();
777
+ }
778
+ getState() {
779
+ return this.snapshot();
780
+ }
781
+ on(event, cb) {
782
+ return this.emitter.on(event, cb);
783
+ }
784
+ off(event, cb) {
785
+ this.emitter.off(event, cb);
786
+ }
787
+ destroy() {
788
+ for (const dispose of this.disposers) dispose();
789
+ this.disposers = [];
790
+ this.provider.destroy();
791
+ this.controls.destroy();
792
+ this.emitter.clear();
793
+ this.root.remove();
794
+ }
795
+ };
796
+
797
+ // src/index.ts
798
+ function createPlayer(container, options) {
799
+ return new Player(container, options);
800
+ }
801
+
802
+ exports.Player = Player;
803
+ exports.createPlayer = createPlayer;
804
+ //# sourceMappingURL=index.cjs.map
805
+ //# sourceMappingURL=index.cjs.map