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