@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/react/index.cjs
CHANGED
|
@@ -452,13 +452,124 @@ function createProvider(source) {
|
|
|
452
452
|
}
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
+
// src/core/Captions.ts
|
|
456
|
+
var TIMING = /^\s*((?:\d+:)?\d{1,2}:\d{2}[.,]\d{1,3})\s+-->\s+((?:\d+:)?\d{1,2}:\d{2}[.,]\d{1,3})/;
|
|
457
|
+
var ENTITIES = {
|
|
458
|
+
amp: "&",
|
|
459
|
+
lt: "<",
|
|
460
|
+
gt: ">",
|
|
461
|
+
quot: '"',
|
|
462
|
+
apos: "'",
|
|
463
|
+
nbsp: "\xA0",
|
|
464
|
+
lrm: "\u200E",
|
|
465
|
+
rlm: "\u200F"
|
|
466
|
+
};
|
|
467
|
+
function toSeconds(stamp) {
|
|
468
|
+
const [clock = "", fraction = "0"] = stamp.replace(",", ".").split(".");
|
|
469
|
+
const seconds = clock.split(":").reduce((total, part) => total * 60 + Number(part), 0);
|
|
470
|
+
return seconds + Number(`0.${fraction}`);
|
|
471
|
+
}
|
|
472
|
+
function plainText(text) {
|
|
473
|
+
return text.replace(/<[^>]*>/g, "").replace(/&(amp|lt|gt|quot|apos|nbsp|lrm|rlm);/g, (_, name) => {
|
|
474
|
+
var _a;
|
|
475
|
+
return (_a = ENTITIES[name]) != null ? _a : "";
|
|
476
|
+
}).replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)));
|
|
477
|
+
}
|
|
478
|
+
function parseVtt(input) {
|
|
479
|
+
var _a;
|
|
480
|
+
const cues = [];
|
|
481
|
+
const blocks = input.replace(/^/, "").replace(/\r\n?/g, "\n").split(/\n{2,}/);
|
|
482
|
+
for (const block of blocks) {
|
|
483
|
+
const lines = block.split("\n");
|
|
484
|
+
const timingAt = lines.findIndex((line) => line.includes("-->"));
|
|
485
|
+
const timing = TIMING.exec((_a = lines[timingAt]) != null ? _a : "");
|
|
486
|
+
if (!(timing == null ? void 0 : timing[1]) || !timing[2]) continue;
|
|
487
|
+
const text = plainText(lines.slice(timingAt + 1).join("\n")).trim();
|
|
488
|
+
if (!text) continue;
|
|
489
|
+
cues.push({ start: toSeconds(timing[1]), end: toSeconds(timing[2]), text });
|
|
490
|
+
}
|
|
491
|
+
return cues.sort((a, b) => a.start - b.start);
|
|
492
|
+
}
|
|
493
|
+
var Captions = class {
|
|
494
|
+
/** `onChange` fires when the active track or track availability changes. */
|
|
495
|
+
constructor(tracks, onChange) {
|
|
496
|
+
this.onChange = onChange;
|
|
497
|
+
this.active = -1;
|
|
498
|
+
this.time = 0;
|
|
499
|
+
this.abort = new AbortController();
|
|
500
|
+
this.el = document.createElement("div");
|
|
501
|
+
this.el.className = "kzp-captions";
|
|
502
|
+
this.el.setAttribute("aria-live", "off");
|
|
503
|
+
this.text = document.createElement("span");
|
|
504
|
+
this.text.className = "kzp-captions-text";
|
|
505
|
+
this.text.hidden = true;
|
|
506
|
+
this.el.appendChild(this.text);
|
|
507
|
+
this.tracks = tracks.map((info) => ({ info, cues: null, failed: false }));
|
|
508
|
+
const preferred = tracks.findIndex((t) => t.default);
|
|
509
|
+
this.tracks.forEach((track, index) => {
|
|
510
|
+
void this.load(track).then(() => {
|
|
511
|
+
if (index === preferred && this.active === -1 && track.cues) this.select(index);
|
|
512
|
+
else this.onChange();
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
async load(track) {
|
|
517
|
+
try {
|
|
518
|
+
const res = await fetch(track.info.src, { signal: this.abort.signal, credentials: "same-origin" });
|
|
519
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
520
|
+
track.cues = parseVtt(await res.text());
|
|
521
|
+
} catch (err) {
|
|
522
|
+
if (this.abort.signal.aborted) return;
|
|
523
|
+
track.failed = true;
|
|
524
|
+
console.warn(`@kodezen/player: could not load captions from ${track.info.src}`, err);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/** Labels for the menu, one per track (falls back to the language, then a number). */
|
|
528
|
+
labels() {
|
|
529
|
+
return this.tracks.map((t, i) => t.info.label || t.info.srclang || `Track ${i + 1}`);
|
|
530
|
+
}
|
|
531
|
+
/** Whether each track loaded (a failed track is hidden from the controls). */
|
|
532
|
+
available() {
|
|
533
|
+
return this.tracks.map((t) => !t.failed);
|
|
534
|
+
}
|
|
535
|
+
getActive() {
|
|
536
|
+
return this.active;
|
|
537
|
+
}
|
|
538
|
+
/** Show track `index`, or turn captions off with -1. */
|
|
539
|
+
select(index) {
|
|
540
|
+
var _a;
|
|
541
|
+
const track = this.tracks[index];
|
|
542
|
+
this.active = track && !track.failed ? index : -1;
|
|
543
|
+
this.el.lang = (_a = track == null ? void 0 : track.info.srclang) != null ? _a : "";
|
|
544
|
+
this.render();
|
|
545
|
+
this.onChange();
|
|
546
|
+
}
|
|
547
|
+
/** Called with the playhead position on every tick and after a seek. */
|
|
548
|
+
update(time) {
|
|
549
|
+
this.time = time;
|
|
550
|
+
this.render();
|
|
551
|
+
}
|
|
552
|
+
render() {
|
|
553
|
+
var _a, _b;
|
|
554
|
+
const cues = (_b = (_a = this.tracks[this.active]) == null ? void 0 : _a.cues) != null ? _b : [];
|
|
555
|
+
const text = cues.filter((cue) => this.time >= cue.start && this.time < cue.end).map((cue) => cue.text).join("\n");
|
|
556
|
+
if (text !== this.text.textContent) this.text.textContent = text;
|
|
557
|
+
this.text.hidden = !text;
|
|
558
|
+
}
|
|
559
|
+
destroy() {
|
|
560
|
+
this.abort.abort();
|
|
561
|
+
this.el.remove();
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
|
|
455
565
|
// src/core/Controls.ts
|
|
456
566
|
var ICONS = {
|
|
457
567
|
play: '<path d="M8 5v14l11-7z"/>',
|
|
458
568
|
pause: '<path d="M6 5h4v14H6zM14 5h4v14h-4z"/>',
|
|
459
569
|
volume: '<path d="M3 10v4h4l5 5V5L7 10H3zm13.5 2a4.5 4.5 0 0 0-2.5-4v8a4.5 4.5 0 0 0 2.5-4z"/>',
|
|
460
570
|
muted: '<path d="M3 10v4h4l5 5V5L7 10H3zm13 2 3 3 1-1-3-3 3-3-1-1-3 3-3-3-1 1 3 3-3 3 1 1z"/>',
|
|
461
|
-
full: '<path d="M4 4h6v2H6v4H4V4zm10 0h6v6h-2V6h-4V4zM4 14h2v4h4v2H4v-6zm14 0h2v6h-6v-2h4v-4z"/>'
|
|
571
|
+
full: '<path d="M4 4h6v2H6v4H4V4zm10 0h6v6h-2V6h-4V4zM4 14h2v4h4v2H4v-6zm14 0h2v6h-6v-2h4v-4z"/>',
|
|
572
|
+
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"/>'
|
|
462
573
|
};
|
|
463
574
|
function icon(paths) {
|
|
464
575
|
return `<svg viewBox="0 0 24 24" aria-hidden="true">${paths}</svg>`;
|
|
@@ -478,20 +589,23 @@ var DEFAULT_CONFIG = {
|
|
|
478
589
|
time: true,
|
|
479
590
|
volume: true,
|
|
480
591
|
speed: true,
|
|
592
|
+
captions: true,
|
|
481
593
|
fullscreen: true
|
|
482
594
|
};
|
|
483
595
|
var Controls = class {
|
|
484
596
|
constructor(cb, opts) {
|
|
485
597
|
this.cb = cb;
|
|
598
|
+
this.ccActive = -1;
|
|
486
599
|
this.scrubbing = false;
|
|
487
600
|
this.seekableMax = Infinity;
|
|
601
|
+
var _a;
|
|
488
602
|
this.config = { ...DEFAULT_CONFIG, ...opts.config };
|
|
489
603
|
this.lockSeek = opts.lockSeek;
|
|
490
604
|
this.el = document.createElement("div");
|
|
491
605
|
this.el.className = "kzp-controls";
|
|
492
|
-
this.build(opts.speeds);
|
|
606
|
+
this.build(opts.speeds, (_a = opts.captionLabels) != null ? _a : []);
|
|
493
607
|
}
|
|
494
|
-
build(speeds) {
|
|
608
|
+
build(speeds, captionLabels) {
|
|
495
609
|
if (this.config.playPause) {
|
|
496
610
|
this.playBtn = this.button("kzp-play", "Play", ICONS.play);
|
|
497
611
|
this.playBtn.addEventListener("click", () => this.cb.onToggle());
|
|
@@ -568,16 +682,72 @@ var Controls = class {
|
|
|
568
682
|
}
|
|
569
683
|
this.speedBtn.addEventListener("click", () => {
|
|
570
684
|
this.speedMenu.hidden = !this.speedMenu.hidden;
|
|
685
|
+
if (this.ccMenu) this.ccMenu.hidden = true;
|
|
571
686
|
});
|
|
572
687
|
wrap.append(this.speedBtn, this.speedMenu);
|
|
573
688
|
this.el.appendChild(wrap);
|
|
574
689
|
}
|
|
690
|
+
if (this.config.captions && captionLabels.length) {
|
|
691
|
+
this.buildCaptions(captionLabels);
|
|
692
|
+
}
|
|
575
693
|
if (this.config.fullscreen) {
|
|
576
694
|
const btn = this.button("kzp-full", "Fullscreen", ICONS.full);
|
|
577
695
|
btn.addEventListener("click", () => this.cb.onFullscreen());
|
|
578
696
|
this.el.appendChild(btn);
|
|
579
697
|
}
|
|
580
698
|
}
|
|
699
|
+
/**
|
|
700
|
+
* One track: the CC button toggles it. Several: it opens a menu (Off + each
|
|
701
|
+
* track), styled like the speed menu. Sits left of the speed control.
|
|
702
|
+
*/
|
|
703
|
+
buildCaptions(labels) {
|
|
704
|
+
const wrap = document.createElement("div");
|
|
705
|
+
wrap.className = "kzp-cc";
|
|
706
|
+
this.ccBtn = this.button("kzp-cc-btn", "Subtitles/closed captions", ICONS.captions);
|
|
707
|
+
this.ccBtn.setAttribute("aria-pressed", "false");
|
|
708
|
+
wrap.appendChild(this.ccBtn);
|
|
709
|
+
if (labels.length === 1) {
|
|
710
|
+
this.ccBtn.addEventListener("click", () => this.cb.onCaptions(this.ccActive === -1 ? 0 : -1));
|
|
711
|
+
} else {
|
|
712
|
+
this.ccMenu = document.createElement("div");
|
|
713
|
+
this.ccMenu.className = "kzp-speed-menu kzp-cc-menu";
|
|
714
|
+
this.ccMenu.hidden = true;
|
|
715
|
+
const items = [{ index: -1, label: "Off" }, ...labels.map((label, index) => ({ index, label }))];
|
|
716
|
+
for (const { index, label } of items) {
|
|
717
|
+
const item = document.createElement("button");
|
|
718
|
+
item.type = "button";
|
|
719
|
+
item.textContent = label;
|
|
720
|
+
item.dataset.track = String(index);
|
|
721
|
+
item.addEventListener("click", () => {
|
|
722
|
+
this.cb.onCaptions(index);
|
|
723
|
+
this.ccMenu.hidden = true;
|
|
724
|
+
});
|
|
725
|
+
this.ccMenu.appendChild(item);
|
|
726
|
+
}
|
|
727
|
+
this.ccBtn.addEventListener("click", () => {
|
|
728
|
+
this.ccMenu.hidden = !this.ccMenu.hidden;
|
|
729
|
+
if (this.speedMenu) this.speedMenu.hidden = true;
|
|
730
|
+
});
|
|
731
|
+
wrap.appendChild(this.ccMenu);
|
|
732
|
+
}
|
|
733
|
+
this.ccWrap = wrap;
|
|
734
|
+
const speed = this.el.querySelector(".kzp-speed");
|
|
735
|
+
this.el.insertBefore(wrap, speed);
|
|
736
|
+
}
|
|
737
|
+
/** Reflect the active caption track, and hide tracks (or the whole control) that failed to load. */
|
|
738
|
+
setCaptions(active, available) {
|
|
739
|
+
var _a;
|
|
740
|
+
this.ccActive = active;
|
|
741
|
+
if (!this.ccBtn || !this.ccWrap) return;
|
|
742
|
+
this.ccWrap.hidden = !available.some(Boolean);
|
|
743
|
+
this.ccBtn.classList.toggle("kzp-cc-btn--on", active !== -1);
|
|
744
|
+
this.ccBtn.setAttribute("aria-pressed", String(active !== -1));
|
|
745
|
+
(_a = this.ccMenu) == null ? void 0 : _a.querySelectorAll("button").forEach((item) => {
|
|
746
|
+
const index = Number(item.dataset.track);
|
|
747
|
+
item.hidden = index !== -1 && !available[index];
|
|
748
|
+
item.classList.toggle("kzp-menu-item--active", index === active);
|
|
749
|
+
});
|
|
750
|
+
}
|
|
581
751
|
button(cls, label, svg) {
|
|
582
752
|
const b = document.createElement("button");
|
|
583
753
|
b.type = "button";
|
|
@@ -627,16 +797,18 @@ var Player = class {
|
|
|
627
797
|
this.container = container;
|
|
628
798
|
this.options = options;
|
|
629
799
|
this.emitter = new Emitter();
|
|
800
|
+
this.captions = null;
|
|
630
801
|
this.furthest = 0;
|
|
631
802
|
this.lastTick = 0;
|
|
632
803
|
this.duration = 0;
|
|
633
804
|
this.playing = false;
|
|
634
805
|
this.ready = false;
|
|
635
806
|
this.disposers = [];
|
|
636
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
807
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
637
808
|
const speeds = (_a = options.speeds) != null ? _a : DEFAULT_SPEEDS;
|
|
638
809
|
this.root = document.createElement("div");
|
|
639
810
|
this.root.className = "kzp" + (options.className ? ` ${options.className}` : "");
|
|
811
|
+
this.root.classList.add(`kzp--${options.source.type}`);
|
|
640
812
|
if (options.lockSeek) this.root.classList.add("kzp--locked");
|
|
641
813
|
this.stage = document.createElement("div");
|
|
642
814
|
this.stage.className = "kzp-stage";
|
|
@@ -647,6 +819,10 @@ var Player = class {
|
|
|
647
819
|
this.stage.appendChild(hitLayer);
|
|
648
820
|
this.provider = createProvider(options.source);
|
|
649
821
|
const caps = this.provider.capabilities;
|
|
822
|
+
if ((_b = options.tracks) == null ? void 0 : _b.length) {
|
|
823
|
+
this.captions = new Captions(options.tracks, () => this.syncCaptions());
|
|
824
|
+
this.root.appendChild(this.captions.el);
|
|
825
|
+
}
|
|
650
826
|
this.controls = new Controls(
|
|
651
827
|
{
|
|
652
828
|
onToggle: () => this.toggle(),
|
|
@@ -664,17 +840,22 @@ var Player = class {
|
|
|
664
840
|
this.provider.setRate(r);
|
|
665
841
|
this.render();
|
|
666
842
|
},
|
|
843
|
+
onCaptions: (index) => {
|
|
844
|
+
var _a2;
|
|
845
|
+
return (_a2 = this.captions) == null ? void 0 : _a2.select(index);
|
|
846
|
+
},
|
|
667
847
|
onFullscreen: () => this.toggleFullscreen()
|
|
668
848
|
},
|
|
669
849
|
{
|
|
670
850
|
config: {
|
|
671
851
|
...options.controls,
|
|
672
|
-
seek: ((
|
|
673
|
-
speed: ((
|
|
674
|
-
volume: ((
|
|
852
|
+
seek: ((_d = (_c = options.controls) == null ? void 0 : _c.seek) != null ? _d : true) && caps.seekable,
|
|
853
|
+
speed: ((_f = (_e = options.controls) == null ? void 0 : _e.speed) != null ? _f : true) && caps.rate,
|
|
854
|
+
volume: ((_h = (_g = options.controls) == null ? void 0 : _g.volume) != null ? _h : true) && caps.volume
|
|
675
855
|
},
|
|
676
856
|
speeds,
|
|
677
|
-
lockSeek: !!options.lockSeek
|
|
857
|
+
lockSeek: !!options.lockSeek,
|
|
858
|
+
captionLabels: (_i = this.captions) == null ? void 0 : _i.labels()
|
|
678
859
|
}
|
|
679
860
|
);
|
|
680
861
|
this.root.appendChild(this.controls.el);
|
|
@@ -727,6 +908,7 @@ var Player = class {
|
|
|
727
908
|
this.render();
|
|
728
909
|
}
|
|
729
910
|
onTimeUpdate(current) {
|
|
911
|
+
var _a;
|
|
730
912
|
const advance = current - this.lastTick;
|
|
731
913
|
if (advance > 0 && advance <= NATURAL_ADVANCE_MAX && current > this.furthest) {
|
|
732
914
|
this.furthest = current;
|
|
@@ -734,9 +916,14 @@ var Player = class {
|
|
|
734
916
|
this.provider.seek(this.furthest);
|
|
735
917
|
}
|
|
736
918
|
this.lastTick = current;
|
|
919
|
+
(_a = this.captions) == null ? void 0 : _a.update(current);
|
|
737
920
|
this.emitter.emit("timeupdate", this.snapshot());
|
|
738
921
|
this.render();
|
|
739
922
|
}
|
|
923
|
+
syncCaptions() {
|
|
924
|
+
if (!this.captions) return;
|
|
925
|
+
this.controls.setCaptions(this.captions.getActive(), this.captions.available());
|
|
926
|
+
}
|
|
740
927
|
snapshot() {
|
|
741
928
|
const current = this.provider.getCurrentTime();
|
|
742
929
|
const duration = this.duration || this.provider.getDuration();
|
|
@@ -770,10 +957,12 @@ var Player = class {
|
|
|
770
957
|
else this.provider.play();
|
|
771
958
|
}
|
|
772
959
|
seek(seconds) {
|
|
960
|
+
var _a;
|
|
773
961
|
if (!this.provider.capabilities.seekable) return;
|
|
774
962
|
const target = this.options.lockSeek && seconds > this.furthest ? this.furthest : Math.max(0, seconds);
|
|
775
963
|
this.provider.seek(target);
|
|
776
964
|
this.lastTick = target;
|
|
965
|
+
(_a = this.captions) == null ? void 0 : _a.update(target);
|
|
777
966
|
this.render();
|
|
778
967
|
}
|
|
779
968
|
setLockSeek(locked) {
|
|
@@ -792,9 +981,11 @@ var Player = class {
|
|
|
792
981
|
this.emitter.off(event, cb);
|
|
793
982
|
}
|
|
794
983
|
destroy() {
|
|
984
|
+
var _a;
|
|
795
985
|
for (const dispose of this.disposers) dispose();
|
|
796
986
|
this.disposers = [];
|
|
797
987
|
this.provider.destroy();
|
|
988
|
+
(_a = this.captions) == null ? void 0 : _a.destroy();
|
|
798
989
|
this.controls.destroy();
|
|
799
990
|
this.emitter.clear();
|
|
800
991
|
this.root.remove();
|
|
@@ -807,7 +998,7 @@ function createPlayer(container, options) {
|
|
|
807
998
|
}
|
|
808
999
|
var KodezenPlayer = react.forwardRef(
|
|
809
1000
|
function KodezenPlayer2(props, ref) {
|
|
810
|
-
var _a, _b;
|
|
1001
|
+
var _a, _b, _c;
|
|
811
1002
|
const hostRef = react.useRef(null);
|
|
812
1003
|
const playerRef = react.useRef(null);
|
|
813
1004
|
const propsRef = react.useRef(props);
|
|
@@ -857,7 +1048,8 @@ var KodezenPlayer = react.forwardRef(
|
|
|
857
1048
|
return (_a2 = playerRef.current) == null ? void 0 : _a2.destroy();
|
|
858
1049
|
}
|
|
859
1050
|
}));
|
|
860
|
-
const
|
|
1051
|
+
const trackKey = ((_a = props.tracks) != null ? _a : []).map((t) => t.src).join(",");
|
|
1052
|
+
const sourceKey = `${props.source.type}|${(_b = props.source.url) != null ? _b : ""}|${(_c = props.source.id) != null ? _c : ""}|${trackKey}`;
|
|
861
1053
|
react.useEffect(() => {
|
|
862
1054
|
if (!hostRef.current) return;
|
|
863
1055
|
const p = propsRef.current;
|
|
@@ -869,7 +1061,8 @@ var KodezenPlayer = react.forwardRef(
|
|
|
869
1061
|
muted: p.muted,
|
|
870
1062
|
speeds: p.speeds,
|
|
871
1063
|
controls: p.controls,
|
|
872
|
-
className: p.className
|
|
1064
|
+
className: p.className,
|
|
1065
|
+
tracks: p.tracks
|
|
873
1066
|
});
|
|
874
1067
|
playerRef.current = player;
|
|
875
1068
|
const offs = [
|