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