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