@kodezen/player 1.0.1 → 1.0.3
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/README.md +21 -1
- package/dist/index.cjs +200 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -3
- package/dist/index.d.ts +17 -3
- package/dist/index.js +200 -9
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +204 -11
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +204 -11
- package/dist/react/index.js.map +1 -1
- package/dist/styles.css +72 -0
- package/dist/{types-TFltFgey.d.cts → types-30yBj4ll.d.cts} +20 -1
- package/dist/{types-TFltFgey.d.ts → types-30yBj4ll.d.ts} +20 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -445,13 +445,124 @@ function createProvider(source) {
|
|
|
445
445
|
}
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
+
// src/core/Captions.ts
|
|
449
|
+
var TIMING = /^\s*((?:\d+:)?\d{1,2}:\d{2}[.,]\d{1,3})\s+-->\s+((?:\d+:)?\d{1,2}:\d{2}[.,]\d{1,3})/;
|
|
450
|
+
var ENTITIES = {
|
|
451
|
+
amp: "&",
|
|
452
|
+
lt: "<",
|
|
453
|
+
gt: ">",
|
|
454
|
+
quot: '"',
|
|
455
|
+
apos: "'",
|
|
456
|
+
nbsp: "\xA0",
|
|
457
|
+
lrm: "\u200E",
|
|
458
|
+
rlm: "\u200F"
|
|
459
|
+
};
|
|
460
|
+
function toSeconds(stamp) {
|
|
461
|
+
const [clock = "", fraction = "0"] = stamp.replace(",", ".").split(".");
|
|
462
|
+
const seconds = clock.split(":").reduce((total, part) => total * 60 + Number(part), 0);
|
|
463
|
+
return seconds + Number(`0.${fraction}`);
|
|
464
|
+
}
|
|
465
|
+
function plainText(text) {
|
|
466
|
+
return text.replace(/<[^>]*>/g, "").replace(/&(amp|lt|gt|quot|apos|nbsp|lrm|rlm);/g, (_, name) => {
|
|
467
|
+
var _a;
|
|
468
|
+
return (_a = ENTITIES[name]) != null ? _a : "";
|
|
469
|
+
}).replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)));
|
|
470
|
+
}
|
|
471
|
+
function parseVtt(input) {
|
|
472
|
+
var _a;
|
|
473
|
+
const cues = [];
|
|
474
|
+
const blocks = input.replace(/^/, "").replace(/\r\n?/g, "\n").split(/\n{2,}/);
|
|
475
|
+
for (const block of blocks) {
|
|
476
|
+
const lines = block.split("\n");
|
|
477
|
+
const timingAt = lines.findIndex((line) => line.includes("-->"));
|
|
478
|
+
const timing = TIMING.exec((_a = lines[timingAt]) != null ? _a : "");
|
|
479
|
+
if (!(timing == null ? void 0 : timing[1]) || !timing[2]) continue;
|
|
480
|
+
const text = plainText(lines.slice(timingAt + 1).join("\n")).trim();
|
|
481
|
+
if (!text) continue;
|
|
482
|
+
cues.push({ start: toSeconds(timing[1]), end: toSeconds(timing[2]), text });
|
|
483
|
+
}
|
|
484
|
+
return cues.sort((a, b) => a.start - b.start);
|
|
485
|
+
}
|
|
486
|
+
var Captions = class {
|
|
487
|
+
/** `onChange` fires when the active track or track availability changes. */
|
|
488
|
+
constructor(tracks, onChange) {
|
|
489
|
+
this.onChange = onChange;
|
|
490
|
+
this.active = -1;
|
|
491
|
+
this.time = 0;
|
|
492
|
+
this.abort = new AbortController();
|
|
493
|
+
this.el = document.createElement("div");
|
|
494
|
+
this.el.className = "kzp-captions";
|
|
495
|
+
this.el.setAttribute("aria-live", "off");
|
|
496
|
+
this.text = document.createElement("span");
|
|
497
|
+
this.text.className = "kzp-captions-text";
|
|
498
|
+
this.text.hidden = true;
|
|
499
|
+
this.el.appendChild(this.text);
|
|
500
|
+
this.tracks = tracks.map((info) => ({ info, cues: null, failed: false }));
|
|
501
|
+
const preferred = tracks.findIndex((t) => t.default);
|
|
502
|
+
this.tracks.forEach((track, index) => {
|
|
503
|
+
void this.load(track).then(() => {
|
|
504
|
+
if (index === preferred && this.active === -1 && track.cues) this.select(index);
|
|
505
|
+
else this.onChange();
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
async load(track) {
|
|
510
|
+
try {
|
|
511
|
+
const res = await fetch(track.info.src, { signal: this.abort.signal, credentials: "same-origin" });
|
|
512
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
513
|
+
track.cues = parseVtt(await res.text());
|
|
514
|
+
} catch (err) {
|
|
515
|
+
if (this.abort.signal.aborted) return;
|
|
516
|
+
track.failed = true;
|
|
517
|
+
console.warn(`@kodezen/player: could not load captions from ${track.info.src}`, err);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/** Labels for the menu, one per track (falls back to the language, then a number). */
|
|
521
|
+
labels() {
|
|
522
|
+
return this.tracks.map((t, i) => t.info.label || t.info.srclang || `Track ${i + 1}`);
|
|
523
|
+
}
|
|
524
|
+
/** Whether each track loaded (a failed track is hidden from the controls). */
|
|
525
|
+
available() {
|
|
526
|
+
return this.tracks.map((t) => !t.failed);
|
|
527
|
+
}
|
|
528
|
+
getActive() {
|
|
529
|
+
return this.active;
|
|
530
|
+
}
|
|
531
|
+
/** Show track `index`, or turn captions off with -1. */
|
|
532
|
+
select(index) {
|
|
533
|
+
var _a;
|
|
534
|
+
const track = this.tracks[index];
|
|
535
|
+
this.active = track && !track.failed ? index : -1;
|
|
536
|
+
this.el.lang = (_a = track == null ? void 0 : track.info.srclang) != null ? _a : "";
|
|
537
|
+
this.render();
|
|
538
|
+
this.onChange();
|
|
539
|
+
}
|
|
540
|
+
/** Called with the playhead position on every tick and after a seek. */
|
|
541
|
+
update(time) {
|
|
542
|
+
this.time = time;
|
|
543
|
+
this.render();
|
|
544
|
+
}
|
|
545
|
+
render() {
|
|
546
|
+
var _a, _b;
|
|
547
|
+
const cues = (_b = (_a = this.tracks[this.active]) == null ? void 0 : _a.cues) != null ? _b : [];
|
|
548
|
+
const text = cues.filter((cue) => this.time >= cue.start && this.time < cue.end).map((cue) => cue.text).join("\n");
|
|
549
|
+
if (text !== this.text.textContent) this.text.textContent = text;
|
|
550
|
+
this.text.hidden = !text;
|
|
551
|
+
}
|
|
552
|
+
destroy() {
|
|
553
|
+
this.abort.abort();
|
|
554
|
+
this.el.remove();
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
|
|
448
558
|
// src/core/Controls.ts
|
|
449
559
|
var ICONS = {
|
|
450
560
|
play: '<path d="M8 5v14l11-7z"/>',
|
|
451
561
|
pause: '<path d="M6 5h4v14H6zM14 5h4v14h-4z"/>',
|
|
452
562
|
volume: '<path d="M3 10v4h4l5 5V5L7 10H3zm13.5 2a4.5 4.5 0 0 0-2.5-4v8a4.5 4.5 0 0 0 2.5-4z"/>',
|
|
453
563
|
muted: '<path d="M3 10v4h4l5 5V5L7 10H3zm13 2 3 3 1-1-3-3 3-3-1-1-3 3-3-3-1 1 3 3-3 3 1 1z"/>',
|
|
454
|
-
full: '<path d="M4 4h6v2H6v4H4V4zm10 0h6v6h-2V6h-4V4zM4 14h2v4h4v2H4v-6zm14 0h2v6h-6v-2h4v-4z"/>'
|
|
564
|
+
full: '<path d="M4 4h6v2H6v4H4V4zm10 0h6v6h-2V6h-4V4zM4 14h2v4h4v2H4v-6zm14 0h2v6h-6v-2h4v-4z"/>',
|
|
565
|
+
captions: '<path d="M19 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v1z"/>'
|
|
455
566
|
};
|
|
456
567
|
function icon(paths) {
|
|
457
568
|
return `<svg viewBox="0 0 24 24" aria-hidden="true">${paths}</svg>`;
|
|
@@ -471,20 +582,23 @@ var DEFAULT_CONFIG = {
|
|
|
471
582
|
time: true,
|
|
472
583
|
volume: true,
|
|
473
584
|
speed: true,
|
|
585
|
+
captions: true,
|
|
474
586
|
fullscreen: true
|
|
475
587
|
};
|
|
476
588
|
var Controls = class {
|
|
477
589
|
constructor(cb, opts) {
|
|
478
590
|
this.cb = cb;
|
|
591
|
+
this.ccActive = -1;
|
|
479
592
|
this.scrubbing = false;
|
|
480
593
|
this.seekableMax = Infinity;
|
|
594
|
+
var _a;
|
|
481
595
|
this.config = { ...DEFAULT_CONFIG, ...opts.config };
|
|
482
596
|
this.lockSeek = opts.lockSeek;
|
|
483
597
|
this.el = document.createElement("div");
|
|
484
598
|
this.el.className = "kzp-controls";
|
|
485
|
-
this.build(opts.speeds);
|
|
599
|
+
this.build(opts.speeds, (_a = opts.captionLabels) != null ? _a : []);
|
|
486
600
|
}
|
|
487
|
-
build(speeds) {
|
|
601
|
+
build(speeds, captionLabels) {
|
|
488
602
|
if (this.config.playPause) {
|
|
489
603
|
this.playBtn = this.button("kzp-play", "Play", ICONS.play);
|
|
490
604
|
this.playBtn.addEventListener("click", () => this.cb.onToggle());
|
|
@@ -561,16 +675,72 @@ var Controls = class {
|
|
|
561
675
|
}
|
|
562
676
|
this.speedBtn.addEventListener("click", () => {
|
|
563
677
|
this.speedMenu.hidden = !this.speedMenu.hidden;
|
|
678
|
+
if (this.ccMenu) this.ccMenu.hidden = true;
|
|
564
679
|
});
|
|
565
680
|
wrap.append(this.speedBtn, this.speedMenu);
|
|
566
681
|
this.el.appendChild(wrap);
|
|
567
682
|
}
|
|
683
|
+
if (this.config.captions && captionLabels.length) {
|
|
684
|
+
this.buildCaptions(captionLabels);
|
|
685
|
+
}
|
|
568
686
|
if (this.config.fullscreen) {
|
|
569
687
|
const btn = this.button("kzp-full", "Fullscreen", ICONS.full);
|
|
570
688
|
btn.addEventListener("click", () => this.cb.onFullscreen());
|
|
571
689
|
this.el.appendChild(btn);
|
|
572
690
|
}
|
|
573
691
|
}
|
|
692
|
+
/**
|
|
693
|
+
* One track: the CC button toggles it. Several: it opens a menu (Off + each
|
|
694
|
+
* track), styled like the speed menu. Sits left of the speed control.
|
|
695
|
+
*/
|
|
696
|
+
buildCaptions(labels) {
|
|
697
|
+
const wrap = document.createElement("div");
|
|
698
|
+
wrap.className = "kzp-cc";
|
|
699
|
+
this.ccBtn = this.button("kzp-cc-btn", "Subtitles/closed captions", ICONS.captions);
|
|
700
|
+
this.ccBtn.setAttribute("aria-pressed", "false");
|
|
701
|
+
wrap.appendChild(this.ccBtn);
|
|
702
|
+
if (labels.length === 1) {
|
|
703
|
+
this.ccBtn.addEventListener("click", () => this.cb.onCaptions(this.ccActive === -1 ? 0 : -1));
|
|
704
|
+
} else {
|
|
705
|
+
this.ccMenu = document.createElement("div");
|
|
706
|
+
this.ccMenu.className = "kzp-speed-menu kzp-cc-menu";
|
|
707
|
+
this.ccMenu.hidden = true;
|
|
708
|
+
const items = [{ index: -1, label: "Off" }, ...labels.map((label, index) => ({ index, label }))];
|
|
709
|
+
for (const { index, label } of items) {
|
|
710
|
+
const item = document.createElement("button");
|
|
711
|
+
item.type = "button";
|
|
712
|
+
item.textContent = label;
|
|
713
|
+
item.dataset.track = String(index);
|
|
714
|
+
item.addEventListener("click", () => {
|
|
715
|
+
this.cb.onCaptions(index);
|
|
716
|
+
this.ccMenu.hidden = true;
|
|
717
|
+
});
|
|
718
|
+
this.ccMenu.appendChild(item);
|
|
719
|
+
}
|
|
720
|
+
this.ccBtn.addEventListener("click", () => {
|
|
721
|
+
this.ccMenu.hidden = !this.ccMenu.hidden;
|
|
722
|
+
if (this.speedMenu) this.speedMenu.hidden = true;
|
|
723
|
+
});
|
|
724
|
+
wrap.appendChild(this.ccMenu);
|
|
725
|
+
}
|
|
726
|
+
this.ccWrap = wrap;
|
|
727
|
+
const speed = this.el.querySelector(".kzp-speed");
|
|
728
|
+
this.el.insertBefore(wrap, speed);
|
|
729
|
+
}
|
|
730
|
+
/** Reflect the active caption track, and hide tracks (or the whole control) that failed to load. */
|
|
731
|
+
setCaptions(active, available) {
|
|
732
|
+
var _a;
|
|
733
|
+
this.ccActive = active;
|
|
734
|
+
if (!this.ccBtn || !this.ccWrap) return;
|
|
735
|
+
this.ccWrap.hidden = !available.some(Boolean);
|
|
736
|
+
this.ccBtn.classList.toggle("kzp-cc-btn--on", active !== -1);
|
|
737
|
+
this.ccBtn.setAttribute("aria-pressed", String(active !== -1));
|
|
738
|
+
(_a = this.ccMenu) == null ? void 0 : _a.querySelectorAll("button").forEach((item) => {
|
|
739
|
+
const index = Number(item.dataset.track);
|
|
740
|
+
item.hidden = index !== -1 && !available[index];
|
|
741
|
+
item.classList.toggle("kzp-menu-item--active", index === active);
|
|
742
|
+
});
|
|
743
|
+
}
|
|
574
744
|
button(cls, label, svg) {
|
|
575
745
|
const b = document.createElement("button");
|
|
576
746
|
b.type = "button";
|
|
@@ -620,16 +790,18 @@ var Player = class {
|
|
|
620
790
|
this.container = container;
|
|
621
791
|
this.options = options;
|
|
622
792
|
this.emitter = new Emitter();
|
|
793
|
+
this.captions = null;
|
|
623
794
|
this.furthest = 0;
|
|
624
795
|
this.lastTick = 0;
|
|
625
796
|
this.duration = 0;
|
|
626
797
|
this.playing = false;
|
|
627
798
|
this.ready = false;
|
|
628
799
|
this.disposers = [];
|
|
629
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
800
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
630
801
|
const speeds = (_a = options.speeds) != null ? _a : DEFAULT_SPEEDS;
|
|
631
802
|
this.root = document.createElement("div");
|
|
632
803
|
this.root.className = "kzp" + (options.className ? ` ${options.className}` : "");
|
|
804
|
+
this.root.classList.add(`kzp--${options.source.type}`);
|
|
633
805
|
if (options.lockSeek) this.root.classList.add("kzp--locked");
|
|
634
806
|
this.stage = document.createElement("div");
|
|
635
807
|
this.stage.className = "kzp-stage";
|
|
@@ -640,6 +812,10 @@ var Player = class {
|
|
|
640
812
|
this.stage.appendChild(hitLayer);
|
|
641
813
|
this.provider = createProvider(options.source);
|
|
642
814
|
const caps = this.provider.capabilities;
|
|
815
|
+
if ((_b = options.tracks) == null ? void 0 : _b.length) {
|
|
816
|
+
this.captions = new Captions(options.tracks, () => this.syncCaptions());
|
|
817
|
+
this.root.appendChild(this.captions.el);
|
|
818
|
+
}
|
|
643
819
|
this.controls = new Controls(
|
|
644
820
|
{
|
|
645
821
|
onToggle: () => this.toggle(),
|
|
@@ -657,17 +833,22 @@ var Player = class {
|
|
|
657
833
|
this.provider.setRate(r);
|
|
658
834
|
this.render();
|
|
659
835
|
},
|
|
836
|
+
onCaptions: (index) => {
|
|
837
|
+
var _a2;
|
|
838
|
+
return (_a2 = this.captions) == null ? void 0 : _a2.select(index);
|
|
839
|
+
},
|
|
660
840
|
onFullscreen: () => this.toggleFullscreen()
|
|
661
841
|
},
|
|
662
842
|
{
|
|
663
843
|
config: {
|
|
664
844
|
...options.controls,
|
|
665
|
-
seek: ((
|
|
666
|
-
speed: ((
|
|
667
|
-
volume: ((
|
|
845
|
+
seek: ((_d = (_c = options.controls) == null ? void 0 : _c.seek) != null ? _d : true) && caps.seekable,
|
|
846
|
+
speed: ((_f = (_e = options.controls) == null ? void 0 : _e.speed) != null ? _f : true) && caps.rate,
|
|
847
|
+
volume: ((_h = (_g = options.controls) == null ? void 0 : _g.volume) != null ? _h : true) && caps.volume
|
|
668
848
|
},
|
|
669
849
|
speeds,
|
|
670
|
-
lockSeek: !!options.lockSeek
|
|
850
|
+
lockSeek: !!options.lockSeek,
|
|
851
|
+
captionLabels: (_i = this.captions) == null ? void 0 : _i.labels()
|
|
671
852
|
}
|
|
672
853
|
);
|
|
673
854
|
this.root.appendChild(this.controls.el);
|
|
@@ -720,6 +901,7 @@ var Player = class {
|
|
|
720
901
|
this.render();
|
|
721
902
|
}
|
|
722
903
|
onTimeUpdate(current) {
|
|
904
|
+
var _a;
|
|
723
905
|
const advance = current - this.lastTick;
|
|
724
906
|
if (advance > 0 && advance <= NATURAL_ADVANCE_MAX && current > this.furthest) {
|
|
725
907
|
this.furthest = current;
|
|
@@ -727,9 +909,14 @@ var Player = class {
|
|
|
727
909
|
this.provider.seek(this.furthest);
|
|
728
910
|
}
|
|
729
911
|
this.lastTick = current;
|
|
912
|
+
(_a = this.captions) == null ? void 0 : _a.update(current);
|
|
730
913
|
this.emitter.emit("timeupdate", this.snapshot());
|
|
731
914
|
this.render();
|
|
732
915
|
}
|
|
916
|
+
syncCaptions() {
|
|
917
|
+
if (!this.captions) return;
|
|
918
|
+
this.controls.setCaptions(this.captions.getActive(), this.captions.available());
|
|
919
|
+
}
|
|
733
920
|
snapshot() {
|
|
734
921
|
const current = this.provider.getCurrentTime();
|
|
735
922
|
const duration = this.duration || this.provider.getDuration();
|
|
@@ -763,10 +950,12 @@ var Player = class {
|
|
|
763
950
|
else this.provider.play();
|
|
764
951
|
}
|
|
765
952
|
seek(seconds) {
|
|
953
|
+
var _a;
|
|
766
954
|
if (!this.provider.capabilities.seekable) return;
|
|
767
955
|
const target = this.options.lockSeek && seconds > this.furthest ? this.furthest : Math.max(0, seconds);
|
|
768
956
|
this.provider.seek(target);
|
|
769
957
|
this.lastTick = target;
|
|
958
|
+
(_a = this.captions) == null ? void 0 : _a.update(target);
|
|
770
959
|
this.render();
|
|
771
960
|
}
|
|
772
961
|
setLockSeek(locked) {
|
|
@@ -785,9 +974,11 @@ var Player = class {
|
|
|
785
974
|
this.emitter.off(event, cb);
|
|
786
975
|
}
|
|
787
976
|
destroy() {
|
|
977
|
+
var _a;
|
|
788
978
|
for (const dispose of this.disposers) dispose();
|
|
789
979
|
this.disposers = [];
|
|
790
980
|
this.provider.destroy();
|
|
981
|
+
(_a = this.captions) == null ? void 0 : _a.destroy();
|
|
791
982
|
this.controls.destroy();
|
|
792
983
|
this.emitter.clear();
|
|
793
984
|
this.root.remove();
|
|
@@ -799,6 +990,6 @@ function createPlayer(container, options) {
|
|
|
799
990
|
return new Player(container, options);
|
|
800
991
|
}
|
|
801
992
|
|
|
802
|
-
export { Player, createPlayer };
|
|
993
|
+
export { Player, createPlayer, parseVtt };
|
|
803
994
|
//# sourceMappingURL=index.js.map
|
|
804
995
|
//# sourceMappingURL=index.js.map
|