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