@f8team/reel-core 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,2014 @@
1
+ 'use strict';
2
+
3
+ // src/errors/registry.ts
4
+ var RETRYABLE_BY_DEFAULT = {
5
+ source: false,
6
+ network: true,
7
+ decode: false,
8
+ unauthorized: false,
9
+ unsupported: false,
10
+ internal: false
11
+ };
12
+ function createPlayerError(input) {
13
+ const error = {
14
+ code: input.code,
15
+ message: input.message,
16
+ retryable: input.retryable ?? RETRYABLE_BY_DEFAULT[input.code]
17
+ };
18
+ if (input.cause !== void 0) error.cause = input.cause;
19
+ if (input.status !== void 0) error.status = input.status;
20
+ if (input.url !== void 0) error.url = input.url;
21
+ return error;
22
+ }
23
+
24
+ // src/errors/classify.ts
25
+ function classifyMediaError(err, url) {
26
+ const code = err?.code ?? 0;
27
+ const message = err?.message || "Media error";
28
+ switch (code) {
29
+ case 1:
30
+ return createPlayerError({ code: "internal", message: "Media playback aborted", url });
31
+ case 2:
32
+ return createPlayerError({ code: "network", message, url, retryable: true });
33
+ case 3:
34
+ return createPlayerError({ code: "decode", message, url });
35
+ case 4:
36
+ return createPlayerError({ code: "unsupported", message, url });
37
+ default:
38
+ return createPlayerError({ code: "internal", message, url, cause: err });
39
+ }
40
+ }
41
+ function classifyUnknown(err, url) {
42
+ if (err instanceof Error) {
43
+ return createPlayerError({
44
+ code: "internal",
45
+ message: err.message || "Unknown error",
46
+ cause: err,
47
+ url
48
+ });
49
+ }
50
+ return createPlayerError({
51
+ code: "internal",
52
+ message: typeof err === "string" ? err : "Unknown error",
53
+ cause: err,
54
+ url
55
+ });
56
+ }
57
+
58
+ // src/events/bus.ts
59
+ function createEventBus() {
60
+ const buckets = /* @__PURE__ */ new Map();
61
+ function on(event, listener) {
62
+ const key = String(event);
63
+ let set = buckets.get(key);
64
+ if (!set) {
65
+ set = /* @__PURE__ */ new Set();
66
+ buckets.set(key, set);
67
+ }
68
+ set.add(listener);
69
+ return () => off(event, listener);
70
+ }
71
+ function off(event, listener) {
72
+ const key = String(event);
73
+ const set = buckets.get(key);
74
+ if (!set) return;
75
+ set.delete(listener);
76
+ if (set.size === 0) buckets.delete(key);
77
+ }
78
+ function emit(event, payload) {
79
+ const key = String(event);
80
+ const set = buckets.get(key);
81
+ if (!set || set.size === 0) return;
82
+ const snapshot = Array.from(set);
83
+ for (const listener of snapshot) {
84
+ try {
85
+ listener(payload);
86
+ } catch (err) {
87
+ console.error(`[@f8team/reel-core] listener for "${key}" threw:`, err);
88
+ }
89
+ }
90
+ }
91
+ function clear() {
92
+ buckets.clear();
93
+ }
94
+ function listenerCount(event) {
95
+ return buckets.get(event)?.size ?? 0;
96
+ }
97
+ return {
98
+ on,
99
+ off,
100
+ emit,
101
+ clear,
102
+ listenerCount
103
+ };
104
+ }
105
+
106
+ // src/plugins/bus.ts
107
+ function createPluginBus({ player, host }) {
108
+ const entries = [];
109
+ function findIndex(name) {
110
+ return entries.findIndex((e) => e.plugin.name === name);
111
+ }
112
+ function register(plugin) {
113
+ if (findIndex(plugin.name) >= 0) {
114
+ throw new Error(`[@f8team/reel-core] plugin "${plugin.name}" is already registered`);
115
+ }
116
+ let teardown = null;
117
+ try {
118
+ const result = plugin.setup(player, host);
119
+ teardown = typeof result === "function" ? result : null;
120
+ } catch (err) {
121
+ throw new Error(
122
+ `[@f8team/reel-core] plugin "${plugin.name}" setup threw: ${err.message}`,
123
+ { cause: err }
124
+ );
125
+ }
126
+ entries.push({ plugin, teardown });
127
+ }
128
+ function unregister(name) {
129
+ const idx = findIndex(name);
130
+ if (idx < 0) return;
131
+ const entry = entries.splice(idx, 1)[0];
132
+ if (!entry) return;
133
+ if (entry.teardown) {
134
+ try {
135
+ entry.teardown();
136
+ } catch (err) {
137
+ console.error(`[@f8team/reel-core] plugin "${name}" teardown threw:`, err);
138
+ }
139
+ }
140
+ }
141
+ function has(name) {
142
+ return findIndex(name) >= 0;
143
+ }
144
+ function list() {
145
+ return entries.map((e) => e.plugin.name);
146
+ }
147
+ function disposeAll() {
148
+ while (entries.length) {
149
+ const entry = entries.pop();
150
+ if (entry?.teardown) {
151
+ try {
152
+ entry.teardown();
153
+ } catch (err) {
154
+ console.error(
155
+ `[@f8team/reel-core] plugin "${entry.plugin.name}" teardown threw on dispose:`,
156
+ err
157
+ );
158
+ }
159
+ }
160
+ }
161
+ }
162
+ return { register, unregister, has, list, disposeAll };
163
+ }
164
+
165
+ // src/plugins/commands.ts
166
+ function createCommandRegistry() {
167
+ const handlers = /* @__PURE__ */ new Map();
168
+ function add(name, handler) {
169
+ if (handlers.has(name)) {
170
+ throw new Error(`[@f8team/reel-core] command "${name}" is already registered`);
171
+ }
172
+ handlers.set(name, handler);
173
+ return () => {
174
+ const current = handlers.get(name);
175
+ if (current === handler) handlers.delete(name);
176
+ };
177
+ }
178
+ function run(name, payload) {
179
+ const handler = handlers.get(name);
180
+ if (!handler) return;
181
+ try {
182
+ handler(payload);
183
+ } catch (err) {
184
+ console.error(`[@f8team/reel-core] command "${name}" threw:`, err);
185
+ }
186
+ }
187
+ function has(name) {
188
+ return handlers.has(name);
189
+ }
190
+ return { add, run, has };
191
+ }
192
+
193
+ // src/plugins/host.ts
194
+ function createControlsRegistry() {
195
+ const slots = /* @__PURE__ */ new Map();
196
+ function contribute(slot, render) {
197
+ let set = slots.get(slot);
198
+ if (!set) {
199
+ set = /* @__PURE__ */ new Set();
200
+ slots.set(slot, set);
201
+ }
202
+ set.add(render);
203
+ return () => {
204
+ slots.get(slot)?.delete(render);
205
+ };
206
+ }
207
+ function snapshot(slot) {
208
+ const set = slots.get(slot);
209
+ return set ? Array.from(set) : [];
210
+ }
211
+ function clear() {
212
+ slots.clear();
213
+ }
214
+ return { contribute, snapshot, clear };
215
+ }
216
+ function createPluginHost({ commands, controls, store, bus }) {
217
+ return {
218
+ controls: {
219
+ contribute: (slot, render) => controls.contribute(slot, render)
220
+ },
221
+ commands,
222
+ store,
223
+ emit: (event, payload) => {
224
+ bus.emit(event, payload);
225
+ }
226
+ };
227
+ }
228
+
229
+ // src/util/url.ts
230
+ function looksLikeHls(input) {
231
+ if (!input) return false;
232
+ const lower = input.toLowerCase();
233
+ if (lower.includes("youtube.com") || lower.includes("youtu.be")) return false;
234
+ if (lower.includes(".m3u8")) return true;
235
+ if (lower.includes("application/vnd.apple.mpegurl")) return true;
236
+ return false;
237
+ }
238
+ function looksLikeYouTube(input) {
239
+ if (!input) return false;
240
+ const lower = input.toLowerCase();
241
+ return lower.includes("youtube.com") || lower.includes("youtu.be");
242
+ }
243
+ function looksLikeProgressive(input) {
244
+ if (!input) return false;
245
+ const lower = input.toLowerCase();
246
+ if (lower.startsWith("blob:") || lower.startsWith("data:")) return true;
247
+ return /\.(mp4|m4v|webm|ogv)(\?|#|$)/.test(lower);
248
+ }
249
+ function detectSourceType(source) {
250
+ if (source.type && source.type !== "auto") return source.type;
251
+ if (looksLikeHls(source.src)) return "hls";
252
+ if (looksLikeYouTube(source.src)) return "youtube";
253
+ if (looksLikeProgressive(source.src)) return "mp4";
254
+ return "native";
255
+ }
256
+ function resolveWithCredentials(policy, url) {
257
+ if (typeof policy === "boolean") return policy;
258
+ if (typeof policy === "function") {
259
+ try {
260
+ return policy(url);
261
+ } catch {
262
+ return false;
263
+ }
264
+ }
265
+ return false;
266
+ }
267
+
268
+ // src/sources/hls.ts
269
+ var defaultLoad = () => {
270
+ return import('hls.js').then((m) => m.default);
271
+ };
272
+ function makeAbortError() {
273
+ const err = new Error("HLS loader aborted");
274
+ err.name = "AbortError";
275
+ return err;
276
+ }
277
+ var HlsLoader = class {
278
+ constructor(options) {
279
+ this.options = options;
280
+ this.hls = null;
281
+ this.video = null;
282
+ this.detachListeners = [];
283
+ this.qualities = [];
284
+ this.currentSource = null;
285
+ this.unauthorizedFired = false;
286
+ // Track every XHR that hls.js spawns so we can abort and remove their
287
+ // readystatechange watcher on `abort()` / `detach()`. Without this set the
288
+ // watcher closure leaks for every media segment (A1 in the review).
289
+ this.liveXhrs = /* @__PURE__ */ new Set();
290
+ this.rejectAttach = null;
291
+ this.manifestTimeout = null;
292
+ this.aborted = false;
293
+ /** `false` idle; `'auto'` → first `LEVEL_SWITCHED` clears; otherwise await this level index. */
294
+ this.qualityAwait = false;
295
+ this.qualitySwitchTimer = null;
296
+ /** Tracks consecutive menu selections so redundant "Auto → Auto" skips the spinner. */
297
+ this.lastChosenQualityWasAuto = false;
298
+ /**
299
+ * `null` — user chose ABR auto (menu shows "Auto"). Real hls.js still exposes
300
+ * `currentLevel >= 0` for the playing rendition; we must not mirror that into UI state.
301
+ */
302
+ this.manualLevelIndex = null;
303
+ }
304
+ async attach(video, source) {
305
+ this.detach();
306
+ this.aborted = false;
307
+ this.video = video;
308
+ this.currentSource = source;
309
+ this.unauthorizedFired = false;
310
+ const Hls = await (this.options.loadRuntime ?? defaultLoad)();
311
+ if (this.aborted) {
312
+ throw makeAbortError();
313
+ }
314
+ if (!Hls.isSupported()) {
315
+ return this.attachNative(video, source);
316
+ }
317
+ return this.attachWithHlsJs(Hls, video, source);
318
+ }
319
+ /**
320
+ * Native-HLS branch (Safari iOS / desktop without MSE). Listeners are
321
+ * pushed into `detachListeners` so that an early `detach()` / `abort()`
322
+ * cleans them up instead of leaking (C3 in the review).
323
+ */
324
+ attachNative(video, source) {
325
+ return new Promise((resolve, reject) => {
326
+ this.rejectAttach = reject;
327
+ let settled = false;
328
+ const settle = (run) => {
329
+ if (settled) return;
330
+ settled = true;
331
+ run();
332
+ };
333
+ const onMeta = () => settle(() => resolve());
334
+ const onError = () => settle(() => reject(new Error("Native HLS playback failed")));
335
+ video.addEventListener("loadedmetadata", onMeta);
336
+ video.addEventListener("error", onError);
337
+ this.detachListeners.push(
338
+ () => video.removeEventListener("loadedmetadata", onMeta),
339
+ () => video.removeEventListener("error", onError),
340
+ () => settle(() => {
341
+ reject(makeAbortError());
342
+ })
343
+ );
344
+ try {
345
+ video.src = source.src;
346
+ video.load();
347
+ } catch (err) {
348
+ settle(() => reject(err instanceof Error ? err : new Error(String(err))));
349
+ }
350
+ });
351
+ }
352
+ attachWithHlsJs(Hls, video, source) {
353
+ const hls = new Hls({
354
+ enableWorker: true,
355
+ xhrSetup: (xhr, url) => this.installXhrWatcher(xhr, url, source)
356
+ });
357
+ this.hls = hls;
358
+ return new Promise((resolve, reject) => {
359
+ this.rejectAttach = reject;
360
+ let settled = false;
361
+ const settle = (run) => {
362
+ if (settled) return;
363
+ settled = true;
364
+ this.clearManifestTimeout();
365
+ run();
366
+ };
367
+ const onParsed = () => {
368
+ this.refreshQualities();
369
+ settle(() => resolve());
370
+ };
371
+ const onSwitched = (...args) => {
372
+ this.onLevelEventuallyStabilized(...args);
373
+ this.refreshActiveQuality();
374
+ };
375
+ const onError = (..._args) => {
376
+ const data = _args[1] ?? {};
377
+ const status = data.response?.code;
378
+ const isHttpError = typeof status === "number" && status >= 400;
379
+ if (data.fatal || isHttpError) {
380
+ const message = data.fatal ? `HLS fatal error: ${data.type ?? "unknown"}` : `HLS load error: HTTP ${status}`;
381
+ this.options.onError?.({
382
+ message,
383
+ cause: data,
384
+ status,
385
+ url: data.response?.url ?? data.url
386
+ });
387
+ settle(() => reject(new Error(message)));
388
+ }
389
+ };
390
+ hls.on(Hls.Events.MANIFEST_PARSED, onParsed);
391
+ hls.on(Hls.Events.LEVEL_SWITCHED, onSwitched);
392
+ hls.on(Hls.Events.ERROR, onError);
393
+ this.detachListeners.push(
394
+ () => hls.off(Hls.Events.MANIFEST_PARSED, onParsed),
395
+ () => hls.off(Hls.Events.LEVEL_SWITCHED, onSwitched),
396
+ () => hls.off(Hls.Events.ERROR, onError),
397
+ () => settle(() => reject(makeAbortError()))
398
+ );
399
+ const timeoutMs = this.options.manifestTimeoutMs ?? 3e4;
400
+ if (timeoutMs > 0) {
401
+ this.manifestTimeout = setTimeout(() => {
402
+ const message = `HLS manifest timeout after ${timeoutMs}ms`;
403
+ this.options.onError?.({ message, cause: { type: "timeout" }, url: source.src });
404
+ settle(() => reject(new Error(message)));
405
+ }, timeoutMs);
406
+ }
407
+ try {
408
+ hls.loadSource(source.src);
409
+ hls.attachMedia(video);
410
+ } catch (err) {
411
+ settle(() => reject(err instanceof Error ? err : new Error(String(err))));
412
+ }
413
+ });
414
+ }
415
+ /**
416
+ * Wire the readystatechange watcher and remove it as soon as the XHR
417
+ * completes — without this cleanup hls.js (which spawns one XHR per segment)
418
+ * leaks a closure per request (A1).
419
+ */
420
+ installXhrWatcher(xhr, url, source) {
421
+ if (resolveWithCredentials(source.withCredentials, url)) {
422
+ xhr.withCredentials = true;
423
+ }
424
+ this.liveXhrs.add(xhr);
425
+ const watcher = () => {
426
+ if (xhr.readyState !== 4) return;
427
+ try {
428
+ if ((xhr.status === 401 || xhr.status === 403) && !this.unauthorizedFired) {
429
+ this.unauthorizedFired = true;
430
+ this.options.onUnauthorized?.({
431
+ url,
432
+ status: xhr.status,
433
+ source
434
+ });
435
+ }
436
+ } finally {
437
+ xhr.removeEventListener("readystatechange", watcher);
438
+ this.liveXhrs.delete(xhr);
439
+ }
440
+ };
441
+ xhr.addEventListener("readystatechange", watcher);
442
+ }
443
+ abort() {
444
+ if (this.aborted) return;
445
+ this.aborted = true;
446
+ this.clearManifestTimeout();
447
+ for (const xhr of [...this.liveXhrs]) {
448
+ try {
449
+ xhr.abort();
450
+ } catch {
451
+ }
452
+ }
453
+ this.liveXhrs.clear();
454
+ const reject = this.rejectAttach;
455
+ this.rejectAttach = null;
456
+ reject?.(makeAbortError());
457
+ }
458
+ detach() {
459
+ this.completeQualityAwaitIfBusy();
460
+ this.abort();
461
+ while (this.detachListeners.length) {
462
+ try {
463
+ this.detachListeners.pop()?.();
464
+ } catch {
465
+ }
466
+ }
467
+ if (this.hls) {
468
+ try {
469
+ this.hls.detachMedia();
470
+ this.hls.destroy();
471
+ } catch {
472
+ }
473
+ this.hls = null;
474
+ }
475
+ if (this.video) {
476
+ try {
477
+ this.video.removeAttribute("src");
478
+ this.video.load();
479
+ } catch {
480
+ }
481
+ this.video = null;
482
+ }
483
+ this.qualities = [];
484
+ this.currentSource = null;
485
+ this.unauthorizedFired = false;
486
+ this.aborted = false;
487
+ this.lastChosenQualityWasAuto = false;
488
+ this.manualLevelIndex = null;
489
+ }
490
+ clearQualitySwitchTimerOnly() {
491
+ if (this.qualitySwitchTimer !== null) {
492
+ clearTimeout(this.qualitySwitchTimer);
493
+ this.qualitySwitchTimer = null;
494
+ }
495
+ }
496
+ armQualitySwitchTimeout() {
497
+ this.clearQualitySwitchTimerOnly();
498
+ this.qualitySwitchTimer = setTimeout(() => {
499
+ this.qualitySwitchTimer = null;
500
+ if (this.qualityAwait !== false) {
501
+ this.completeQualityAwaitIfBusy();
502
+ }
503
+ }, 12e3);
504
+ }
505
+ /** Start or refresh awaited UI for a rendition change (rapid toggles reuse one spinner). */
506
+ armQualityAwait(kind) {
507
+ const wasIdle = this.qualityAwait === false;
508
+ this.qualityAwait = kind;
509
+ if (wasIdle) this.options.onQualitySwitch?.(true);
510
+ this.armQualitySwitchTimeout();
511
+ }
512
+ resolveQualityAwaitIfDone(levelIdxHint) {
513
+ const awaiting = this.qualityAwait;
514
+ if (awaiting === false) return;
515
+ const engineLevel = this.hls !== null && typeof this.hls.currentLevel === "number" ? this.hls.currentLevel : levelIdxHint;
516
+ const done = awaiting === "auto" || typeof awaiting === "number" && awaiting === engineLevel;
517
+ if (done) this.completeQualityAwaitIfBusy();
518
+ }
519
+ onLevelEventuallyStabilized(...args) {
520
+ let levelIdx;
521
+ for (let i = args.length - 1; i >= 0; i--) {
522
+ const a = args[i];
523
+ if (a !== null && typeof a === "object" && typeof a.level === "number") {
524
+ levelIdx = a.level;
525
+ break;
526
+ }
527
+ }
528
+ if (levelIdx === void 0) levelIdx = this.hls?.currentLevel;
529
+ if (typeof levelIdx === "number") this.resolveQualityAwaitIfDone(levelIdx);
530
+ }
531
+ completeQualityAwaitIfBusy() {
532
+ if (this.qualityAwait === false) return;
533
+ this.qualityAwait = false;
534
+ this.clearQualitySwitchTimerOnly();
535
+ this.options.onQualitySwitch?.(false);
536
+ }
537
+ clearManifestTimeout() {
538
+ if (this.manifestTimeout !== null) {
539
+ clearTimeout(this.manifestTimeout);
540
+ this.manifestTimeout = null;
541
+ }
542
+ }
543
+ getQualities() {
544
+ return this.qualities.slice();
545
+ }
546
+ setQuality(level) {
547
+ if (!this.hls) return;
548
+ if (level === "auto") {
549
+ this.manualLevelIndex = null;
550
+ this.hls.currentLevel = -1;
551
+ this.options.onActiveQuality?.(null, true);
552
+ if (!this.lastChosenQualityWasAuto) {
553
+ this.armQualityAwait("auto");
554
+ }
555
+ this.lastChosenQualityWasAuto = true;
556
+ queueMicrotask(() => {
557
+ this.completeQualityAwaitIfBusy();
558
+ });
559
+ return;
560
+ }
561
+ let idx = this.qualities.findIndex((q) => q.id === level.id);
562
+ if (idx < 0 && typeof level.height === "number" && level.height > 0) {
563
+ const byHeight = this.qualities.findIndex((q) => q.height === level.height);
564
+ if (byHeight >= 0) {
565
+ idx = byHeight;
566
+ }
567
+ }
568
+ if (idx < 0) {
569
+ return;
570
+ }
571
+ const resolved = this.qualities[idx];
572
+ if (!resolved) return;
573
+ this.manualLevelIndex = idx;
574
+ this.lastChosenQualityWasAuto = false;
575
+ if (this.hls.currentLevel === idx) {
576
+ this.options.onActiveQuality?.(resolved, false);
577
+ return;
578
+ }
579
+ if (this.hls.currentLevel !== -1) {
580
+ this.armQualityAwait(idx);
581
+ }
582
+ this.hls.currentLevel = idx;
583
+ this.options.onActiveQuality?.(resolved, false);
584
+ queueMicrotask(() => {
585
+ if (!this.hls) return;
586
+ this.resolveQualityAwaitIfDone(this.hls.currentLevel);
587
+ });
588
+ }
589
+ refreshQualities() {
590
+ if (!this.hls) return;
591
+ const next = this.hls.levels.map((lvl, i) => ({
592
+ id: String(i),
593
+ height: lvl.height ?? 0,
594
+ bitrate: lvl.bitrate ?? 0,
595
+ label: lvl.name ?? (lvl.height ? `${lvl.height}p` : `Level ${i + 1}`)
596
+ }));
597
+ this.qualities = next;
598
+ this.options.onQualities?.(next);
599
+ this.refreshActiveQuality();
600
+ }
601
+ refreshActiveQuality() {
602
+ if (!this.hls) return;
603
+ if (this.manualLevelIndex === null) {
604
+ this.options.onActiveQuality?.(null, true);
605
+ return;
606
+ }
607
+ const locked = this.qualities[this.manualLevelIndex] ?? null;
608
+ this.options.onActiveQuality?.(locked, false);
609
+ }
610
+ /** @internal — for tests. */
611
+ _getInternalState() {
612
+ return { source: this.currentSource, qualities: this.qualities };
613
+ }
614
+ };
615
+ function createHlsProvider(options = {}) {
616
+ return {
617
+ name: "hls",
618
+ canHandle: (source) => {
619
+ if (source.type === "hls") return true;
620
+ const type = detectSourceType(source);
621
+ return type === "hls" || type === "auto" && looksLikeHls(source.src);
622
+ },
623
+ createLoader: () => new HlsLoader(options)
624
+ };
625
+ }
626
+
627
+ // src/sources/native.ts
628
+ var NativeLoader = class {
629
+ constructor() {
630
+ this.video = null;
631
+ this.cleanups = [];
632
+ }
633
+ async attach(video, source) {
634
+ this.detach();
635
+ this.video = video;
636
+ return new Promise((resolve, reject) => {
637
+ const onMeta = () => {
638
+ cleanupListeners();
639
+ resolve();
640
+ };
641
+ const onError = () => {
642
+ cleanupListeners();
643
+ const err = video.error;
644
+ const message = err ? `MediaError code=${err.code} message=${err.message}` : "Native loader failed";
645
+ reject(new Error(message));
646
+ };
647
+ const cleanupListeners = () => {
648
+ video.removeEventListener("loadedmetadata", onMeta);
649
+ video.removeEventListener("error", onError);
650
+ };
651
+ video.addEventListener("loadedmetadata", onMeta);
652
+ video.addEventListener("error", onError);
653
+ this.cleanups.push(cleanupListeners);
654
+ video.src = source.src;
655
+ video.load();
656
+ });
657
+ }
658
+ detach() {
659
+ while (this.cleanups.length) {
660
+ try {
661
+ this.cleanups.pop()?.();
662
+ } catch {
663
+ }
664
+ }
665
+ if (this.video) {
666
+ try {
667
+ this.video.removeAttribute("src");
668
+ this.video.load();
669
+ } catch {
670
+ }
671
+ this.video = null;
672
+ }
673
+ }
674
+ };
675
+ var nativeProvider = {
676
+ name: "native",
677
+ canHandle: (source) => {
678
+ if (source.type === "native") return true;
679
+ if (source.type === "mp4") return true;
680
+ if (source.type && source.type !== "auto") return false;
681
+ const type = detectSourceType(source);
682
+ if (type === "mp4") return true;
683
+ if (type === "hls" || type === "youtube") return false;
684
+ return "maybe";
685
+ },
686
+ createLoader: () => new NativeLoader()
687
+ };
688
+
689
+ // src/sources/registry.ts
690
+ function createSourceRegistry() {
691
+ const providers = [];
692
+ function register(provider) {
693
+ if (providers.some((p) => p.name === provider.name)) {
694
+ throw new Error(
695
+ `[@f8team/reel-core] source provider "${provider.name}" is already registered`
696
+ );
697
+ }
698
+ providers.push(provider);
699
+ return () => unregister(provider.name);
700
+ }
701
+ function unregister(name) {
702
+ const idx = providers.findIndex((p) => p.name === name);
703
+ if (idx >= 0) providers.splice(idx, 1);
704
+ }
705
+ function resolve(source) {
706
+ let fallback = null;
707
+ for (const p of providers) {
708
+ const verdict = p.canHandle(source);
709
+ if (verdict === true) return p;
710
+ if (verdict === "maybe" && !fallback) fallback = p;
711
+ }
712
+ return fallback;
713
+ }
714
+ function list() {
715
+ return providers.slice();
716
+ }
717
+ function clear() {
718
+ providers.length = 0;
719
+ }
720
+ return { register, unregister, resolve, list, clear };
721
+ }
722
+
723
+ // src/sources/youtube.ts
724
+ var YT_SCRIPT_SRC = "https://www.youtube.com/iframe_api";
725
+ var defaultLoad2 = () => {
726
+ if (typeof window === "undefined") {
727
+ return Promise.reject(new Error("YouTube source provider requires a browser environment"));
728
+ }
729
+ const w = window;
730
+ if (w.YT && typeof w.YT.Player === "function") return Promise.resolve(w.YT);
731
+ return new Promise((resolve, reject) => {
732
+ const existing = document.querySelector(`script[src="${YT_SCRIPT_SRC}"]`);
733
+ const onReady = () => {
734
+ if (w.YT && typeof w.YT.Player === "function") resolve(w.YT);
735
+ else reject(new Error("YouTube IFrame API loaded without YT.Player"));
736
+ };
737
+ const previousReady = w.onYouTubeIframeAPIReady;
738
+ w.onYouTubeIframeAPIReady = () => {
739
+ previousReady?.();
740
+ onReady();
741
+ };
742
+ if (!existing) {
743
+ const script = document.createElement("script");
744
+ script.src = YT_SCRIPT_SRC;
745
+ script.async = true;
746
+ script.onerror = () => reject(new Error("Failed to load YouTube IFrame API"));
747
+ document.head.appendChild(script);
748
+ }
749
+ });
750
+ };
751
+ function extractYouTubeId(url) {
752
+ const m = url.match(/[?&]v=([^&]+)/) ?? url.match(/youtu\.be\/([^?&/]+)/) ?? url.match(/youtube\.com\/embed\/([^?&/]+)/) ?? url.match(/youtube\.com\/shorts\/([^?&/]+)/);
753
+ return m?.[1] ?? null;
754
+ }
755
+ var YT_HIDDEN_ATTR = "data-reel-yt-hidden";
756
+ var staticParentWarned = false;
757
+ var YouTubeLoader = class {
758
+ constructor(options) {
759
+ this.options = options;
760
+ this.yt = null;
761
+ this.video = null;
762
+ this.host = null;
763
+ this.timeIntervalId = null;
764
+ this.duration = 0;
765
+ this.rejectAttach = null;
766
+ this.aborted = false;
767
+ }
768
+ async attach(video, source) {
769
+ this.detach();
770
+ this.aborted = false;
771
+ const id = extractYouTubeId(source.src);
772
+ if (!id) {
773
+ throw new Error(`Cannot extract YouTube videoId from "${source.src}"`);
774
+ }
775
+ this.video = video;
776
+ const YT = await (this.options.loadRuntime ?? defaultLoad2)();
777
+ if (this.aborted) {
778
+ throw makeYtAbortError();
779
+ }
780
+ const parent = video.parentElement;
781
+ if (!parent) {
782
+ throw new Error("YouTube source provider requires <video> to be attached to the DOM");
783
+ }
784
+ if (typeof window !== "undefined" && !staticParentWarned) {
785
+ try {
786
+ const computed = window.getComputedStyle(parent).position;
787
+ if (computed === "static") {
788
+ staticParentWarned = true;
789
+ console.warn(
790
+ "[@f8team/reel-core] YouTube source: <video>'s parent has `position: static`. Set the player container to `position: relative` (or absolute/fixed) so the YouTube iframe can fill the stage."
791
+ );
792
+ }
793
+ } catch {
794
+ }
795
+ }
796
+ const host = document.createElement("div");
797
+ host.setAttribute("data-reel-yt-host", "");
798
+ host.style.cssText = "position:absolute;inset:0;width:100%;height:100%;overflow:hidden;background:#000;";
799
+ parent.appendChild(host);
800
+ this.host = host;
801
+ video.setAttribute(YT_HIDDEN_ATTR, "");
802
+ return new Promise((resolve, reject) => {
803
+ this.rejectAttach = reject;
804
+ let settled = false;
805
+ const settle = (run) => {
806
+ if (settled) return;
807
+ settled = true;
808
+ run();
809
+ };
810
+ try {
811
+ this.yt = new YT.Player(host, {
812
+ videoId: id,
813
+ // 100% so the iframe fills the host div rather than using default px dimensions.
814
+ width: "100%",
815
+ height: "100%",
816
+ playerVars: {
817
+ playsinline: 1,
818
+ modestbranding: 1,
819
+ rel: 0,
820
+ // enablejsapi is implicitly set by the IFrame API; explicit = good practice.
821
+ enablejsapi: 1
822
+ },
823
+ events: {
824
+ onReady: () => {
825
+ const iframe = host.querySelector("iframe");
826
+ if (iframe) {
827
+ iframe.style.cssText = "width:100%;height:100%;border:none;display:block;";
828
+ }
829
+ try {
830
+ this.duration = this.yt?.getDuration() ?? 0;
831
+ if (this.duration > 0) {
832
+ this.options.onDuration?.(this.duration);
833
+ }
834
+ } catch {
835
+ }
836
+ this.startTimeBridge();
837
+ this.options.onStateChange?.("ready");
838
+ settle(() => resolve());
839
+ },
840
+ onStateChange: ({ data }) => {
841
+ switch (data) {
842
+ case YT.PlayerState.PLAYING:
843
+ this.options.onStateChange?.("playing");
844
+ break;
845
+ case YT.PlayerState.PAUSED:
846
+ this.options.onStateChange?.("paused");
847
+ break;
848
+ case YT.PlayerState.ENDED:
849
+ this.options.onStateChange?.("ended");
850
+ break;
851
+ case YT.PlayerState.BUFFERING:
852
+ this.options.onStateChange?.("buffering");
853
+ break;
854
+ default:
855
+ break;
856
+ }
857
+ },
858
+ onError: ({ data }) => {
859
+ const err = { message: `YouTube error code=${data}`, cause: { code: data } };
860
+ this.options.onError?.(err);
861
+ settle(() => reject(new Error(err.message)));
862
+ }
863
+ }
864
+ });
865
+ } catch (err) {
866
+ settle(() => reject(err instanceof Error ? err : new Error(String(err))));
867
+ }
868
+ });
869
+ }
870
+ abort() {
871
+ if (this.aborted) return;
872
+ this.aborted = true;
873
+ const reject = this.rejectAttach;
874
+ this.rejectAttach = null;
875
+ reject?.(makeYtAbortError());
876
+ }
877
+ detach() {
878
+ this.abort();
879
+ if (this.timeIntervalId !== null) {
880
+ clearInterval(this.timeIntervalId);
881
+ this.timeIntervalId = null;
882
+ }
883
+ if (this.yt) {
884
+ try {
885
+ this.yt.destroy();
886
+ } catch {
887
+ }
888
+ this.yt = null;
889
+ }
890
+ if (this.host && this.host.parentElement) {
891
+ try {
892
+ this.host.parentElement.removeChild(this.host);
893
+ } catch {
894
+ }
895
+ }
896
+ this.host = null;
897
+ if (this.video) {
898
+ this.video.removeAttribute(YT_HIDDEN_ATTR);
899
+ this.video = null;
900
+ }
901
+ this.duration = 0;
902
+ this.aborted = false;
903
+ }
904
+ startTimeBridge() {
905
+ if (this.timeIntervalId !== null) return;
906
+ const interval = this.options.timeBridgeIntervalMs ?? 250;
907
+ this.timeIntervalId = setInterval(() => {
908
+ if (!this.yt) return;
909
+ try {
910
+ const currentTime = this.yt.getCurrentTime();
911
+ this.options.onTimeUpdate?.(currentTime);
912
+ const duration = this.yt.getDuration();
913
+ if (duration > 0 && duration !== this.duration) {
914
+ this.duration = duration;
915
+ this.options.onDuration?.(duration);
916
+ }
917
+ } catch {
918
+ }
919
+ }, interval);
920
+ }
921
+ /** @internal */
922
+ _instance() {
923
+ return this.yt;
924
+ }
925
+ };
926
+ function makeYtAbortError() {
927
+ const err = new Error("YouTube loader aborted");
928
+ err.name = "AbortError";
929
+ return err;
930
+ }
931
+ function createYouTubeProvider(options = {}) {
932
+ return {
933
+ name: "youtube",
934
+ canHandle: (source) => {
935
+ if (source.type === "youtube") return true;
936
+ const type = detectSourceType(source);
937
+ return type === "youtube" || type === "auto" && looksLikeYouTube(source.src);
938
+ },
939
+ createLoader: () => new YouTubeLoader(options)
940
+ };
941
+ }
942
+
943
+ // src/state/machine.ts
944
+ var STAY = { effects: Object.freeze([]) };
945
+ var noop = (status) => ({ next: status, ...STAY });
946
+ function reduce(status, event) {
947
+ if (event.type === "dispose") {
948
+ if (status === "idle") return noop("idle");
949
+ return { next: "idle", effects: [{ type: "detachSource" }] };
950
+ }
951
+ switch (status) {
952
+ case "idle":
953
+ if (event.type === "setSource") {
954
+ if (event.source === null) return noop("idle");
955
+ return {
956
+ next: "loading",
957
+ effects: [{ type: "clearError" }, { type: "attachSource", source: event.source }]
958
+ };
959
+ }
960
+ return noop("idle");
961
+ case "loading":
962
+ switch (event.type) {
963
+ case "loaded":
964
+ return noop("ready");
965
+ case "loadFailed":
966
+ return { next: "error", effects: [{ type: "setError", error: event.error }] };
967
+ case "setSource":
968
+ if (event.source === null) {
969
+ return { next: "idle", effects: [{ type: "detachSource" }] };
970
+ }
971
+ return {
972
+ next: "loading",
973
+ effects: [
974
+ { type: "detachSource" },
975
+ { type: "clearError" },
976
+ { type: "attachSource", source: event.source }
977
+ ]
978
+ };
979
+ default:
980
+ return noop("loading");
981
+ }
982
+ case "ready":
983
+ switch (event.type) {
984
+ case "play":
985
+ return { next: "playing", effects: [{ type: "mediaPlay" }] };
986
+ case "runtimeError":
987
+ return { next: "error", effects: [{ type: "setError", error: event.error }] };
988
+ case "setSource":
989
+ if (event.source === null) {
990
+ return { next: "idle", effects: [{ type: "detachSource" }] };
991
+ }
992
+ return {
993
+ next: "loading",
994
+ effects: [
995
+ { type: "detachSource" },
996
+ { type: "clearError" },
997
+ { type: "attachSource", source: event.source }
998
+ ]
999
+ };
1000
+ default:
1001
+ return noop("ready");
1002
+ }
1003
+ case "playing":
1004
+ switch (event.type) {
1005
+ case "pause":
1006
+ return { next: "paused", effects: [{ type: "mediaPause" }] };
1007
+ case "ended":
1008
+ return noop("ended");
1009
+ case "runtimeError":
1010
+ return { next: "error", effects: [{ type: "setError", error: event.error }] };
1011
+ case "setSource":
1012
+ if (event.source === null) {
1013
+ return { next: "idle", effects: [{ type: "detachSource" }] };
1014
+ }
1015
+ return {
1016
+ next: "loading",
1017
+ effects: [
1018
+ { type: "detachSource" },
1019
+ { type: "clearError" },
1020
+ { type: "attachSource", source: event.source }
1021
+ ]
1022
+ };
1023
+ default:
1024
+ return noop("playing");
1025
+ }
1026
+ case "paused":
1027
+ switch (event.type) {
1028
+ case "play":
1029
+ return { next: "playing", effects: [{ type: "mediaPlay" }] };
1030
+ case "ended":
1031
+ return noop("ended");
1032
+ case "runtimeError":
1033
+ return { next: "error", effects: [{ type: "setError", error: event.error }] };
1034
+ case "setSource":
1035
+ if (event.source === null) {
1036
+ return { next: "idle", effects: [{ type: "detachSource" }] };
1037
+ }
1038
+ return {
1039
+ next: "loading",
1040
+ effects: [
1041
+ { type: "detachSource" },
1042
+ { type: "clearError" },
1043
+ { type: "attachSource", source: event.source }
1044
+ ]
1045
+ };
1046
+ default:
1047
+ return noop("paused");
1048
+ }
1049
+ case "ended":
1050
+ switch (event.type) {
1051
+ case "play":
1052
+ return {
1053
+ next: "playing",
1054
+ effects: [{ type: "mediaSeek", seconds: 0 }, { type: "mediaPlay" }]
1055
+ };
1056
+ case "setSource":
1057
+ if (event.source === null) {
1058
+ return { next: "idle", effects: [{ type: "detachSource" }] };
1059
+ }
1060
+ return {
1061
+ next: "loading",
1062
+ effects: [
1063
+ { type: "detachSource" },
1064
+ { type: "clearError" },
1065
+ { type: "attachSource", source: event.source }
1066
+ ]
1067
+ };
1068
+ default:
1069
+ return noop("ended");
1070
+ }
1071
+ case "error":
1072
+ switch (event.type) {
1073
+ case "retry": {
1074
+ return { next: "loading", effects: [{ type: "clearError" }] };
1075
+ }
1076
+ case "setSource":
1077
+ if (event.source === null) {
1078
+ return { next: "idle", effects: [{ type: "detachSource" }] };
1079
+ }
1080
+ return {
1081
+ next: "loading",
1082
+ effects: [
1083
+ { type: "detachSource" },
1084
+ { type: "clearError" },
1085
+ { type: "attachSource", source: event.source }
1086
+ ]
1087
+ };
1088
+ default:
1089
+ return noop("error");
1090
+ }
1091
+ default: {
1092
+ const _exhaustive = status;
1093
+ return _exhaustive;
1094
+ }
1095
+ }
1096
+ }
1097
+
1098
+ // src/state/store.ts
1099
+ var defaultEquals = (a, b) => Object.is(a, b);
1100
+ function createStore(initial, options = {}) {
1101
+ const schedule = options.scheduleFlush ?? queueMicrotask;
1102
+ const defaultIsEqual = options.isEqual ?? defaultEquals;
1103
+ let state = initial;
1104
+ const subscribers = /* @__PURE__ */ new Set();
1105
+ let dirty = false;
1106
+ let disposed = false;
1107
+ function flush() {
1108
+ dirty = false;
1109
+ if (disposed) return;
1110
+ const snapshot = Array.from(subscribers);
1111
+ for (const sub of snapshot) {
1112
+ const next = sub.selector(state);
1113
+ if (sub.isEqual(next, sub.last)) continue;
1114
+ sub.last = next;
1115
+ try {
1116
+ sub.listener(next);
1117
+ } catch (err) {
1118
+ console.error("[@f8team/reel-core] store subscriber threw:", err);
1119
+ }
1120
+ }
1121
+ }
1122
+ function getState() {
1123
+ return state;
1124
+ }
1125
+ function setState(patch) {
1126
+ if (disposed) return;
1127
+ const next = typeof patch === "function" ? patch(state) : patch;
1128
+ state = { ...state, ...next };
1129
+ if (!dirty) {
1130
+ dirty = true;
1131
+ schedule(flush);
1132
+ }
1133
+ }
1134
+ function subscribe(selector, listener) {
1135
+ if (disposed) {
1136
+ return () => {
1137
+ };
1138
+ }
1139
+ const sub = {
1140
+ selector,
1141
+ listener,
1142
+ last: selector(state),
1143
+ isEqual: defaultIsEqual
1144
+ };
1145
+ subscribers.add(sub);
1146
+ return () => {
1147
+ subscribers.delete(sub);
1148
+ };
1149
+ }
1150
+ function dispose() {
1151
+ if (disposed) return;
1152
+ disposed = true;
1153
+ subscribers.clear();
1154
+ }
1155
+ return { getState, setState, subscribe, dispose };
1156
+ }
1157
+
1158
+ // src/createPlayer.ts
1159
+ var NATIVE_EVENTS = [
1160
+ "loadedmetadata",
1161
+ "durationchange",
1162
+ "timeupdate",
1163
+ "play",
1164
+ "pause",
1165
+ "ended",
1166
+ "error",
1167
+ "volumechange",
1168
+ "ratechange",
1169
+ "seeking",
1170
+ "seeked",
1171
+ "waiting",
1172
+ "playing"
1173
+ ];
1174
+ function makeInitialState(options) {
1175
+ return {
1176
+ status: "idle",
1177
+ source: options.source ?? null,
1178
+ currentTime: 0,
1179
+ duration: 0,
1180
+ buffered: [],
1181
+ playbackRate: options.playbackRate ?? 1,
1182
+ volume: options.volume ?? 1,
1183
+ muted: options.muted ?? options.autoplay === "muted",
1184
+ videoWidth: 0,
1185
+ videoHeight: 0,
1186
+ pip: false,
1187
+ fullscreen: false,
1188
+ qualities: [],
1189
+ activeQuality: null,
1190
+ error: null
1191
+ };
1192
+ }
1193
+ function bufferedToRanges(buffered) {
1194
+ if (!buffered) return [];
1195
+ const out = [];
1196
+ for (let i = 0; i < buffered.length; i++) {
1197
+ out.push({ start: buffered.start(i), end: buffered.end(i) });
1198
+ }
1199
+ return out;
1200
+ }
1201
+ function createPlayer(options = {}, internal = {}) {
1202
+ const store = createStore(makeInitialState(options), internal.storeOptions);
1203
+ const bus = createEventBus();
1204
+ const commands = createCommandRegistry();
1205
+ const controls = createControlsRegistry();
1206
+ let status = "idle";
1207
+ let video = null;
1208
+ let activeLoader = null;
1209
+ let pendingAttachAbort = null;
1210
+ let disposed = false;
1211
+ let initialOptionsApplied = false;
1212
+ let pendingPlay = false;
1213
+ const registry = createSourceRegistry();
1214
+ function emitUnauthorized(payload) {
1215
+ bus.emit("unauthorized", payload);
1216
+ options.hooks?.onUnauthorized?.(payload);
1217
+ }
1218
+ function applyError(err) {
1219
+ store.setState({ error: err });
1220
+ bus.emit("error", err);
1221
+ options.hooks?.onError?.(err);
1222
+ }
1223
+ function setQualities(qualities) {
1224
+ store.setState({ qualities });
1225
+ }
1226
+ function setActiveQuality(quality, auto) {
1227
+ store.setState({ activeQuality: quality });
1228
+ bus.emit("qualitychange", { quality, auto });
1229
+ }
1230
+ commands.add("hls:setQuality", (level) => {
1231
+ if (level === null) {
1232
+ bus.emit("qualitypreference", { qualityHeight: null });
1233
+ } else if (level !== null && typeof level === "object" && typeof level.height === "number") {
1234
+ bus.emit("qualitypreference", { qualityHeight: level.height });
1235
+ }
1236
+ const loader = activeLoader;
1237
+ if (typeof loader?.setQuality === "function") {
1238
+ loader.setQuality(level === null ? "auto" : level);
1239
+ }
1240
+ });
1241
+ for (const provider of internal.extraProviders ?? []) registry.register(provider);
1242
+ registry.register(
1243
+ createHlsProvider({
1244
+ onUnauthorized: (e) => emitUnauthorized(e),
1245
+ onQualities: (q) => setQualities(q),
1246
+ onActiveQuality: (q, auto) => setActiveQuality(q, auto),
1247
+ onError: (err) => applyError(createPlayerError({ ...err, code: "network" })),
1248
+ onQualitySwitch: (active) => bus.emit("qualityswitch", { active })
1249
+ })
1250
+ );
1251
+ registry.register(
1252
+ createYouTubeProvider({
1253
+ onError: (err) => applyError(createPlayerError({ ...err, code: "source" })),
1254
+ // Bridge YouTube's polled time/duration into the same store fields and
1255
+ // bus events that hls.js / native engines populate. Without this, the
1256
+ // seek bar, transcripts, resume-position and analytics plugins would
1257
+ // never tick on YouTube sources (A3).
1258
+ onTimeUpdate: (currentTime) => {
1259
+ const duration = store.getState().duration;
1260
+ store.setState({ currentTime });
1261
+ bus.emit("timeupdate", { currentTime, playedSeconds: currentTime, duration });
1262
+ },
1263
+ onDuration: (duration) => {
1264
+ store.setState({ duration });
1265
+ bus.emit("durationchange", { duration });
1266
+ }
1267
+ })
1268
+ );
1269
+ registry.register(nativeProvider);
1270
+ const playerRef = {};
1271
+ const host = createPluginHost({
1272
+ commands,
1273
+ controls,
1274
+ store: { getState: store.getState, subscribe: store.subscribe },
1275
+ bus
1276
+ });
1277
+ const plugins = createPluginBus({ player: playerRef, host });
1278
+ function dispatch(event) {
1279
+ if (disposed) return;
1280
+ const prev = status;
1281
+ const result = reduce(status, event);
1282
+ status = result.next;
1283
+ store.setState({ status });
1284
+ for (const effect of result.effects) applyEffect(effect);
1285
+ if (prev !== "ready" && status === "ready") {
1286
+ bus.emit("ready", { duration: store.getState().duration });
1287
+ }
1288
+ }
1289
+ function applyEffect(effect) {
1290
+ switch (effect.type) {
1291
+ case "attachSource":
1292
+ runAttachSource(effect.source);
1293
+ return;
1294
+ case "detachSource":
1295
+ runDetachSource();
1296
+ return;
1297
+ case "mediaPlay":
1298
+ runMediaPlay();
1299
+ return;
1300
+ case "mediaPause":
1301
+ runMediaPause();
1302
+ return;
1303
+ case "mediaSeek":
1304
+ runMediaSeek(effect.seconds);
1305
+ return;
1306
+ case "clearError":
1307
+ if (store.getState().error) store.setState({ error: null });
1308
+ return;
1309
+ case "setError":
1310
+ applyError(effect.error);
1311
+ return;
1312
+ default: {
1313
+ const _exhaustive = effect;
1314
+ return _exhaustive;
1315
+ }
1316
+ }
1317
+ }
1318
+ function runAttachSource(source) {
1319
+ pendingAttachAbort?.();
1320
+ pendingAttachAbort = null;
1321
+ store.setState({
1322
+ source,
1323
+ currentTime: 0,
1324
+ duration: 0,
1325
+ buffered: [],
1326
+ qualities: [],
1327
+ activeQuality: null
1328
+ });
1329
+ bus.emit("qualitychange", { quality: null, auto: true });
1330
+ if (!video) return;
1331
+ const provider = registry.resolve(source);
1332
+ if (!provider) {
1333
+ dispatch({
1334
+ type: "loadFailed",
1335
+ error: createPlayerError({
1336
+ code: "unsupported",
1337
+ message: `No source provider for ${source.src}`
1338
+ })
1339
+ });
1340
+ return;
1341
+ }
1342
+ const loader = provider.createLoader();
1343
+ activeLoader = loader;
1344
+ let aborted = false;
1345
+ pendingAttachAbort = () => {
1346
+ aborted = true;
1347
+ try {
1348
+ loader.abort?.();
1349
+ } catch (err) {
1350
+ console.error("[@f8team/reel-core] loader.abort() threw:", err);
1351
+ }
1352
+ };
1353
+ loader.attach(video, source).then(() => {
1354
+ if (aborted) return;
1355
+ if (!video) return;
1356
+ store.setState({
1357
+ duration: Number.isFinite(video.duration) ? video.duration : 0,
1358
+ videoWidth: video.videoWidth,
1359
+ videoHeight: video.videoHeight,
1360
+ buffered: bufferedToRanges(video.buffered)
1361
+ });
1362
+ dispatch({ type: "loaded" });
1363
+ }).catch((err) => {
1364
+ if (aborted) return;
1365
+ const playerError = classifyUnknown(err, source.src);
1366
+ dispatch({ type: "loadFailed", error: playerError });
1367
+ });
1368
+ }
1369
+ function runDetachSource() {
1370
+ pendingAttachAbort?.();
1371
+ pendingAttachAbort = null;
1372
+ if (activeLoader) {
1373
+ try {
1374
+ activeLoader.detach();
1375
+ } catch (err) {
1376
+ console.error("[@f8team/reel-core] active loader detach threw:", err);
1377
+ }
1378
+ activeLoader = null;
1379
+ }
1380
+ store.setState({
1381
+ source: null,
1382
+ duration: 0,
1383
+ currentTime: 0,
1384
+ buffered: [],
1385
+ qualities: [],
1386
+ activeQuality: null
1387
+ });
1388
+ }
1389
+ function runMediaPlay() {
1390
+ if (!video) return;
1391
+ const result = video.play();
1392
+ if (result && typeof result.then === "function") {
1393
+ result.catch((err) => {
1394
+ dispatch({
1395
+ type: "runtimeError",
1396
+ error: classifyUnknown(err)
1397
+ });
1398
+ });
1399
+ }
1400
+ }
1401
+ function runMediaPause() {
1402
+ if (!video) return;
1403
+ try {
1404
+ video.pause();
1405
+ } catch {
1406
+ }
1407
+ }
1408
+ function runMediaSeek(seconds) {
1409
+ if (!video) return;
1410
+ try {
1411
+ video.currentTime = seconds;
1412
+ } catch {
1413
+ }
1414
+ }
1415
+ const nativeListeners = [];
1416
+ function bindNativeEvents(target) {
1417
+ function on2(name, fn) {
1418
+ const handler = (e) => fn(e);
1419
+ target.addEventListener(name, handler);
1420
+ nativeListeners.push(() => target.removeEventListener(name, handler));
1421
+ }
1422
+ for (const _ of NATIVE_EVENTS) {
1423
+ }
1424
+ on2("loadedmetadata", () => {
1425
+ store.setState({
1426
+ duration: Number.isFinite(target.duration) ? target.duration : 0,
1427
+ videoWidth: target.videoWidth,
1428
+ videoHeight: target.videoHeight,
1429
+ buffered: bufferedToRanges(target.buffered)
1430
+ });
1431
+ if (status === "loading") dispatch({ type: "loaded" });
1432
+ });
1433
+ on2("durationchange", () => {
1434
+ const duration = Number.isFinite(target.duration) ? target.duration : 0;
1435
+ store.setState({ duration });
1436
+ bus.emit("durationchange", { duration });
1437
+ });
1438
+ on2("timeupdate", () => {
1439
+ const currentTime = target.currentTime;
1440
+ store.setState({ currentTime, buffered: bufferedToRanges(target.buffered) });
1441
+ bus.emit("timeupdate", {
1442
+ currentTime,
1443
+ playedSeconds: currentTime,
1444
+ duration: target.duration
1445
+ });
1446
+ });
1447
+ on2("play", () => {
1448
+ bus.emit("play");
1449
+ dispatch({ type: "play" });
1450
+ });
1451
+ on2("pause", () => {
1452
+ bus.emit("pause");
1453
+ dispatch({ type: "pause" });
1454
+ });
1455
+ on2("ended", () => {
1456
+ bus.emit("ended");
1457
+ dispatch({ type: "ended" });
1458
+ });
1459
+ on2("error", () => {
1460
+ const err = classifyMediaError(target.error, target.currentSrc);
1461
+ if (status === "loading") dispatch({ type: "loadFailed", error: err });
1462
+ else dispatch({ type: "runtimeError", error: err });
1463
+ });
1464
+ on2("volumechange", () => {
1465
+ store.setState({ volume: target.volume, muted: target.muted });
1466
+ bus.emit("volumechange", { volume: target.volume, muted: target.muted });
1467
+ });
1468
+ on2("ratechange", () => {
1469
+ store.setState({ playbackRate: target.playbackRate });
1470
+ bus.emit("ratechange", { playbackRate: target.playbackRate });
1471
+ });
1472
+ on2("seeking", () => {
1473
+ bus.emit("seeking", { time: target.currentTime });
1474
+ bus.emit("buffering", { isBuffering: true });
1475
+ });
1476
+ on2("seeked", () => {
1477
+ bus.emit("seeked", { time: target.currentTime });
1478
+ if (target.paused) {
1479
+ bus.emit("buffering", { isBuffering: false });
1480
+ return;
1481
+ }
1482
+ if (target.readyState >= 3) {
1483
+ bus.emit("buffering", { isBuffering: false });
1484
+ }
1485
+ });
1486
+ on2("waiting", () => {
1487
+ if (target.readyState < 2) return;
1488
+ bus.emit("buffering", { isBuffering: true });
1489
+ });
1490
+ on2("playing", () => {
1491
+ bus.emit("buffering", { isBuffering: false });
1492
+ });
1493
+ if (typeof document !== "undefined") {
1494
+ const onFullscreenChange = () => {
1495
+ const isFullscreen = !!document.fullscreenElement;
1496
+ store.setState({ fullscreen: isFullscreen });
1497
+ bus.emit("fullscreenchange", { fullscreen: isFullscreen });
1498
+ };
1499
+ document.addEventListener("fullscreenchange", onFullscreenChange);
1500
+ nativeListeners.push(
1501
+ () => document.removeEventListener("fullscreenchange", onFullscreenChange)
1502
+ );
1503
+ }
1504
+ }
1505
+ function unbindNativeEvents() {
1506
+ while (nativeListeners.length) {
1507
+ try {
1508
+ nativeListeners.pop()?.();
1509
+ } catch {
1510
+ }
1511
+ }
1512
+ }
1513
+ function applyInitialOptions() {
1514
+ if (initialOptionsApplied || !video) return;
1515
+ if (typeof options.volume === "number") video.volume = options.volume;
1516
+ if (typeof options.muted === "boolean") video.muted = options.muted;
1517
+ if (typeof options.playbackRate === "number") video.playbackRate = options.playbackRate;
1518
+ if (options.poster) video.poster = options.poster;
1519
+ if (options.preload) video.preload = options.preload;
1520
+ if (options.playsInline ?? true) {
1521
+ video.playsInline = true;
1522
+ video.setAttribute("webkit-playsinline", "");
1523
+ }
1524
+ if ("crossOrigin" in options) {
1525
+ if (options.crossOrigin) video.crossOrigin = options.crossOrigin;
1526
+ } else {
1527
+ video.crossOrigin = "anonymous";
1528
+ }
1529
+ if (options.loop) video.loop = true;
1530
+ initialOptionsApplied = true;
1531
+ }
1532
+ const attachDisposers = [];
1533
+ function flushAttachDisposers() {
1534
+ while (attachDisposers.length) {
1535
+ try {
1536
+ attachDisposers.pop()?.();
1537
+ } catch {
1538
+ }
1539
+ }
1540
+ }
1541
+ function applyStartTimeNow() {
1542
+ if (video && typeof options.startTime === "number" && options.startTime > 0) {
1543
+ video.currentTime = options.startTime;
1544
+ }
1545
+ }
1546
+ function applyAutoplayNow() {
1547
+ if (!options.autoplay || options.autoplay === "off") return;
1548
+ if (options.autoplay === "muted" && video) video.muted = true;
1549
+ playerRef.play().catch(() => {
1550
+ });
1551
+ }
1552
+ async function attach(el) {
1553
+ if (disposed) throw new Error("[@f8team/reel-core] cannot attach: player disposed");
1554
+ if (video === el) return;
1555
+ if (video) detach();
1556
+ video = el;
1557
+ bindNativeEvents(el);
1558
+ applyInitialOptions();
1559
+ if (options.source) dispatch({ type: "setSource", source: options.source });
1560
+ const wantStartTime = typeof options.startTime === "number" && options.startTime > 0;
1561
+ const wantAutoplay = !!options.autoplay && options.autoplay !== "off" && !!options.source;
1562
+ if (status === "ready") {
1563
+ if (wantStartTime) applyStartTimeNow();
1564
+ if (wantAutoplay) applyAutoplayNow();
1565
+ } else {
1566
+ if (wantStartTime) {
1567
+ const off2 = bus.on("ready", () => {
1568
+ applyStartTimeNow();
1569
+ off2();
1570
+ });
1571
+ attachDisposers.push(off2);
1572
+ }
1573
+ if (wantAutoplay) {
1574
+ const off2 = bus.on("ready", () => {
1575
+ applyAutoplayNow();
1576
+ off2();
1577
+ });
1578
+ attachDisposers.push(off2);
1579
+ }
1580
+ }
1581
+ }
1582
+ function detach() {
1583
+ pendingAttachAbort?.();
1584
+ pendingAttachAbort = null;
1585
+ flushAttachDisposers();
1586
+ unbindNativeEvents();
1587
+ if (activeLoader) {
1588
+ try {
1589
+ activeLoader.detach();
1590
+ } catch {
1591
+ }
1592
+ activeLoader = null;
1593
+ }
1594
+ video = null;
1595
+ }
1596
+ function dispose() {
1597
+ if (disposed) return;
1598
+ disposed = true;
1599
+ plugins.disposeAll();
1600
+ dispatch({ type: "dispose" });
1601
+ detach();
1602
+ bus.clear();
1603
+ controls.clear();
1604
+ store.dispose();
1605
+ }
1606
+ function setSource(source) {
1607
+ pendingPlay = false;
1608
+ dispatch({ type: "setSource", source });
1609
+ }
1610
+ function getSource() {
1611
+ return store.getState().source;
1612
+ }
1613
+ function retry() {
1614
+ const current = store.getState().source;
1615
+ if (!current) return false;
1616
+ dispatch({ type: "setSource", source: current });
1617
+ return true;
1618
+ }
1619
+ async function play() {
1620
+ if (disposed) throw new Error("[@f8team/reel-core] cannot play: player disposed");
1621
+ if (!video) throw new Error("[@f8team/reel-core] cannot play: no <video> attached");
1622
+ if (!store.getState().source) throw new Error("[@f8team/reel-core] cannot play: no source set");
1623
+ if (status === "error") {
1624
+ pendingPlay = false;
1625
+ if (!retry()) {
1626
+ throw new Error("[@f8team/reel-core] cannot play: error state without replayable source");
1627
+ }
1628
+ }
1629
+ if (status === "loading") {
1630
+ if (pendingPlay) {
1631
+ return;
1632
+ }
1633
+ pendingPlay = true;
1634
+ const off2 = bus.on("ready", () => {
1635
+ off2();
1636
+ if (!pendingPlay) return;
1637
+ pendingPlay = false;
1638
+ if (disposed || !video || !store.getState().source) return;
1639
+ dispatch({ type: "play" });
1640
+ });
1641
+ attachDisposers.push(() => {
1642
+ pendingPlay = false;
1643
+ off2();
1644
+ });
1645
+ return;
1646
+ }
1647
+ dispatch({ type: "play" });
1648
+ }
1649
+ function pause() {
1650
+ pendingPlay = false;
1651
+ dispatch({ type: "pause" });
1652
+ }
1653
+ function paused() {
1654
+ if (!video) return true;
1655
+ return video.paused;
1656
+ }
1657
+ function seekTo(seconds) {
1658
+ if (!Number.isFinite(seconds)) return;
1659
+ if (!video) return;
1660
+ const duration = Number.isFinite(video.duration) ? video.duration : Number.POSITIVE_INFINITY;
1661
+ const clamped = Math.max(0, Math.min(seconds, duration));
1662
+ video.currentTime = clamped;
1663
+ }
1664
+ function syncVolumeMutedStoreFromVideo() {
1665
+ if (!video) return;
1666
+ store.setState({ volume: video.volume, muted: video.muted });
1667
+ }
1668
+ function syncPlaybackRateStoreFromVideo() {
1669
+ if (!video) return;
1670
+ store.setState({ playbackRate: video.playbackRate });
1671
+ }
1672
+ function setPlaybackRate(rate) {
1673
+ if (!Number.isFinite(rate) || rate <= 0) return;
1674
+ if (!video) return;
1675
+ video.playbackRate = rate;
1676
+ syncPlaybackRateStoreFromVideo();
1677
+ }
1678
+ function setVolume(volume) {
1679
+ if (!Number.isFinite(volume)) return;
1680
+ if (!video) return;
1681
+ video.volume = Math.max(0, Math.min(1, volume));
1682
+ syncVolumeMutedStoreFromVideo();
1683
+ }
1684
+ function setMuted(muted) {
1685
+ if (!video) return;
1686
+ video.muted = muted;
1687
+ syncVolumeMutedStoreFromVideo();
1688
+ }
1689
+ function getState() {
1690
+ return store.getState();
1691
+ }
1692
+ function getCurrentTime() {
1693
+ return store.getState().currentTime;
1694
+ }
1695
+ function getDuration() {
1696
+ return store.getState().duration;
1697
+ }
1698
+ function getBuffered() {
1699
+ return store.getState().buffered.slice();
1700
+ }
1701
+ function on(event, handler) {
1702
+ return bus.on(event, handler);
1703
+ }
1704
+ function off(event, handler) {
1705
+ bus.off(event, handler);
1706
+ }
1707
+ function subscribe(selector, listener) {
1708
+ return store.subscribe(selector, listener);
1709
+ }
1710
+ function use(plugin) {
1711
+ plugins.register(plugin);
1712
+ }
1713
+ function removePlugin(name) {
1714
+ plugins.unregister(name);
1715
+ }
1716
+ Object.assign(playerRef, {
1717
+ setSource,
1718
+ getSource,
1719
+ retry,
1720
+ play,
1721
+ pause,
1722
+ paused,
1723
+ seekTo,
1724
+ setPlaybackRate,
1725
+ setVolume,
1726
+ setMuted,
1727
+ getState,
1728
+ getCurrentTime,
1729
+ getDuration,
1730
+ getBuffered,
1731
+ on,
1732
+ off,
1733
+ subscribe,
1734
+ use,
1735
+ removePlugin,
1736
+ commands,
1737
+ attach,
1738
+ detach,
1739
+ dispose
1740
+ });
1741
+ for (const plugin of options.plugins ?? []) plugins.register(plugin);
1742
+ return playerRef;
1743
+ }
1744
+
1745
+ // src/plugins/definePlugin.ts
1746
+ function definePlugin(spec) {
1747
+ return spec;
1748
+ }
1749
+
1750
+ // src/theme/tokens.ts
1751
+ var TOKEN_TO_CSS_VAR = {
1752
+ "color-bg": "--reel-color-bg",
1753
+ "color-fg": "--reel-color-fg",
1754
+ "color-accent": "--reel-color-accent",
1755
+ "color-overlay": "--reel-color-overlay",
1756
+ "control-size": "--reel-control-size",
1757
+ "control-radius": "--reel-control-radius",
1758
+ "control-gap": "--reel-control-gap",
1759
+ "control-padding": "--reel-control-padding",
1760
+ "focus-ring-color": "--reel-focus-ring-color",
1761
+ "focus-ring-width": "--reel-focus-ring-width",
1762
+ "seekbar-height": "--reel-seekbar-height",
1763
+ "seekbar-buffered": "--reel-seekbar-buffered",
1764
+ "seekbar-played": "--reel-seekbar-played",
1765
+ "seekbar-knob-size": "--reel-seekbar-knob-size",
1766
+ "font-family": "--reel-font-family",
1767
+ "font-size-sm": "--reel-font-size-sm",
1768
+ "font-size-md": "--reel-font-size-md",
1769
+ "motion-fast": "--reel-motion-fast",
1770
+ "motion-base": "--reel-motion-base"
1771
+ };
1772
+ var DEFAULT_TOKENS = {
1773
+ "color-bg": "#0d0d0d",
1774
+ "color-fg": "#ffffff",
1775
+ "color-accent": "#ff5722",
1776
+ "color-overlay": "rgba(0, 0, 0, 0.55)",
1777
+ "control-size": "3.6rem",
1778
+ "control-radius": "0.8rem",
1779
+ "control-gap": "0.8rem",
1780
+ "control-padding": "0.8rem",
1781
+ "focus-ring-color": "#3ea6ff",
1782
+ "focus-ring-width": "0.2rem",
1783
+ "seekbar-height": "0.4rem",
1784
+ "seekbar-buffered": "rgba(255, 255, 255, 0.35)",
1785
+ "seekbar-played": "var(--reel-color-accent)",
1786
+ "seekbar-knob-size": "1.2rem",
1787
+ "font-family": "system-ui, -apple-system, 'Segoe UI', sans-serif",
1788
+ "font-size-sm": "1.2rem",
1789
+ "font-size-md": "1.4rem",
1790
+ "motion-fast": "120ms",
1791
+ "motion-base": "200ms"
1792
+ };
1793
+ var THEME_PRESETS = {
1794
+ classroom: {
1795
+ "color-bg": "#0e1117",
1796
+ "color-accent": "#ff6b00",
1797
+ "control-size": "4.0rem",
1798
+ "seekbar-height": "0.4rem"
1799
+ },
1800
+ story: {
1801
+ "color-bg": "transparent",
1802
+ "color-overlay": "rgba(0, 0, 0, 0.45)",
1803
+ "control-size": "4.4rem",
1804
+ "seekbar-height": "0.3rem"
1805
+ },
1806
+ admin: {
1807
+ "color-bg": "#1a1d23",
1808
+ "color-accent": "#3ea6ff",
1809
+ "control-size": "3.2rem",
1810
+ "seekbar-height": "0.5rem"
1811
+ },
1812
+ minimal: {
1813
+ "color-bg": "transparent",
1814
+ "control-size": "3.0rem",
1815
+ "seekbar-height": "0.3rem"
1816
+ }
1817
+ };
1818
+
1819
+ // src/theme/apply.ts
1820
+ function resolveTokens(args = {}) {
1821
+ const out = {};
1822
+ if (args.applyDefaults !== false) Object.assign(out, DEFAULT_TOKENS);
1823
+ if (args.preset) Object.assign(out, THEME_PRESETS[args.preset]);
1824
+ if (args.overrides) Object.assign(out, args.overrides);
1825
+ return out;
1826
+ }
1827
+ function applyTheme(element, args = {}) {
1828
+ const tokens = resolveTokens(args);
1829
+ const previous = {};
1830
+ for (const [key, value] of Object.entries(tokens)) {
1831
+ if (typeof value !== "string") continue;
1832
+ const cssVar = TOKEN_TO_CSS_VAR[key];
1833
+ if (!cssVar) continue;
1834
+ previous[key] = element.style.getPropertyValue(cssVar);
1835
+ element.style.setProperty(cssVar, value);
1836
+ }
1837
+ return () => {
1838
+ for (const [key, prev] of Object.entries(previous)) {
1839
+ const cssVar = TOKEN_TO_CSS_VAR[key];
1840
+ if (!cssVar) continue;
1841
+ if (prev) element.style.setProperty(cssVar, prev);
1842
+ else element.style.removeProperty(cssVar);
1843
+ }
1844
+ };
1845
+ }
1846
+
1847
+ // src/a11y/announce.ts
1848
+ var REGION_ID = "reel-live-region";
1849
+ var region = null;
1850
+ function ensureRegion() {
1851
+ if (typeof document === "undefined") return null;
1852
+ if (region && region.isConnected) return region;
1853
+ const existing = document.getElementById(REGION_ID);
1854
+ if (existing) {
1855
+ region = existing;
1856
+ return region;
1857
+ }
1858
+ const node = document.createElement("div");
1859
+ node.id = REGION_ID;
1860
+ node.setAttribute("aria-live", "polite");
1861
+ node.setAttribute("aria-atomic", "true");
1862
+ node.setAttribute("role", "status");
1863
+ Object.assign(node.style, {
1864
+ position: "absolute",
1865
+ width: "1px",
1866
+ height: "1px",
1867
+ padding: "0",
1868
+ margin: "-1px",
1869
+ overflow: "hidden",
1870
+ clip: "rect(0,0,0,0)",
1871
+ whiteSpace: "nowrap",
1872
+ border: "0"
1873
+ });
1874
+ document.body.appendChild(node);
1875
+ region = node;
1876
+ return region;
1877
+ }
1878
+ function announce(message) {
1879
+ if (!message) return;
1880
+ const node = ensureRegion();
1881
+ if (!node) return;
1882
+ if (node.textContent === message) {
1883
+ node.textContent = "";
1884
+ }
1885
+ queueMicrotask(() => {
1886
+ if (node.isConnected) node.textContent = message;
1887
+ });
1888
+ }
1889
+
1890
+ // src/a11y/focusTrap.ts
1891
+ var FOCUSABLE_SELECTOR = [
1892
+ "a[href]",
1893
+ "area[href]",
1894
+ "input:not([disabled])",
1895
+ "select:not([disabled])",
1896
+ "textarea:not([disabled])",
1897
+ "button:not([disabled])",
1898
+ "iframe",
1899
+ "object",
1900
+ "embed",
1901
+ "[tabindex]:not([tabindex='-1'])",
1902
+ "[contenteditable]"
1903
+ ].join(",");
1904
+ function isFocusable(el) {
1905
+ if (!(el instanceof HTMLElement)) return false;
1906
+ if (el.hidden) return false;
1907
+ if (el.tabIndex === -1) return false;
1908
+ return true;
1909
+ }
1910
+ function focusables(container) {
1911
+ const all = Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
1912
+ return all.filter(isFocusable);
1913
+ }
1914
+ function createFocusTrap(container) {
1915
+ let active = false;
1916
+ let previouslyFocused = null;
1917
+ function onKeyDown(event) {
1918
+ if (!active) return;
1919
+ if (event.key !== "Tab") return;
1920
+ const items = focusables(container);
1921
+ if (items.length === 0) {
1922
+ event.preventDefault();
1923
+ return;
1924
+ }
1925
+ const first = items[0];
1926
+ const last = items[items.length - 1];
1927
+ if (!first || !last) return;
1928
+ const current = document.activeElement;
1929
+ if (event.shiftKey) {
1930
+ if (current === first || !container.contains(current)) {
1931
+ event.preventDefault();
1932
+ last.focus();
1933
+ }
1934
+ } else if (current === last || !container.contains(current)) {
1935
+ event.preventDefault();
1936
+ first.focus();
1937
+ }
1938
+ }
1939
+ function activate(opts = {}) {
1940
+ if (active) return;
1941
+ active = true;
1942
+ previouslyFocused = document.activeElement;
1943
+ container.addEventListener("keydown", onKeyDown, true);
1944
+ if (opts.focusFirst) {
1945
+ const items = focusables(container);
1946
+ items[0]?.focus();
1947
+ }
1948
+ }
1949
+ function deactivate(opts = {}) {
1950
+ if (!active) return;
1951
+ active = false;
1952
+ container.removeEventListener("keydown", onKeyDown, true);
1953
+ if (opts.restoreFocus && previouslyFocused instanceof HTMLElement) {
1954
+ try {
1955
+ previouslyFocused.focus();
1956
+ } catch {
1957
+ }
1958
+ }
1959
+ previouslyFocused = null;
1960
+ }
1961
+ return { activate, deactivate };
1962
+ }
1963
+
1964
+ // src/util/time.ts
1965
+ var TIME_RE = /^(?:(\d+):)?(\d{1,2}):(\d{1,2})(?:\.(\d+))?$/;
1966
+ function parseTime(input) {
1967
+ if (typeof input !== "string") return Number.NaN;
1968
+ const trimmed = input.trim();
1969
+ const match = TIME_RE.exec(trimmed);
1970
+ if (!match) return Number.NaN;
1971
+ const [, hStr, mStr, sStr, fracStr] = match;
1972
+ const h = hStr ? Number.parseInt(hStr, 10) : 0;
1973
+ const m = Number.parseInt(mStr ?? "0", 10);
1974
+ const s = Number.parseInt(sStr ?? "0", 10);
1975
+ const frac = fracStr ? Number.parseFloat(`0.${fracStr}`) : 0;
1976
+ if (m > 59 || s > 59) return Number.NaN;
1977
+ return h * 3600 + m * 60 + s + frac;
1978
+ }
1979
+ function formatTime(seconds) {
1980
+ if (!Number.isFinite(seconds) || seconds < 0) return "0:00";
1981
+ const total = Math.floor(seconds);
1982
+ const h = Math.floor(total / 3600);
1983
+ const m = Math.floor(total % 3600 / 60);
1984
+ const s = total % 60;
1985
+ const pad = (n, len = 2) => n.toString().padStart(len, "0");
1986
+ if (h > 0) return `${h}:${pad(m)}:${pad(s)}`;
1987
+ return `${m}:${pad(s)}`;
1988
+ }
1989
+ function toSeconds(input) {
1990
+ if (typeof input === "number") {
1991
+ if (!Number.isFinite(input)) return Number.NaN;
1992
+ return input < 0 ? 0 : input;
1993
+ }
1994
+ return parseTime(input);
1995
+ }
1996
+
1997
+ exports.DEFAULT_TOKENS = DEFAULT_TOKENS;
1998
+ exports.THEME_PRESETS = THEME_PRESETS;
1999
+ exports.TOKEN_TO_CSS_VAR = TOKEN_TO_CSS_VAR;
2000
+ exports.announce = announce;
2001
+ exports.applyTheme = applyTheme;
2002
+ exports.createFocusTrap = createFocusTrap;
2003
+ exports.createPlayer = createPlayer;
2004
+ exports.definePlugin = definePlugin;
2005
+ exports.detectSourceType = detectSourceType;
2006
+ exports.formatTime = formatTime;
2007
+ exports.looksLikeHls = looksLikeHls;
2008
+ exports.looksLikeProgressive = looksLikeProgressive;
2009
+ exports.looksLikeYouTube = looksLikeYouTube;
2010
+ exports.parseTime = parseTime;
2011
+ exports.resolveTokens = resolveTokens;
2012
+ exports.toSeconds = toSeconds;
2013
+ //# sourceMappingURL=index.cjs.map
2014
+ //# sourceMappingURL=index.cjs.map