@kodezen/player 1.0.2 → 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 +199 -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 +199 -9
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +203 -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 +203 -11
- package/dist/react/index.js.map +1 -1
- package/dist/styles.css +56 -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,13 +795,14 @@ 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}` : "");
|
|
@@ -646,6 +817,10 @@ var Player = class {
|
|
|
646
817
|
this.stage.appendChild(hitLayer);
|
|
647
818
|
this.provider = createProvider(options.source);
|
|
648
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
|
+
}
|
|
649
824
|
this.controls = new Controls(
|
|
650
825
|
{
|
|
651
826
|
onToggle: () => this.toggle(),
|
|
@@ -663,17 +838,22 @@ var Player = class {
|
|
|
663
838
|
this.provider.setRate(r);
|
|
664
839
|
this.render();
|
|
665
840
|
},
|
|
841
|
+
onCaptions: (index) => {
|
|
842
|
+
var _a2;
|
|
843
|
+
return (_a2 = this.captions) == null ? void 0 : _a2.select(index);
|
|
844
|
+
},
|
|
666
845
|
onFullscreen: () => this.toggleFullscreen()
|
|
667
846
|
},
|
|
668
847
|
{
|
|
669
848
|
config: {
|
|
670
849
|
...options.controls,
|
|
671
|
-
seek: ((
|
|
672
|
-
speed: ((
|
|
673
|
-
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
|
|
674
853
|
},
|
|
675
854
|
speeds,
|
|
676
|
-
lockSeek: !!options.lockSeek
|
|
855
|
+
lockSeek: !!options.lockSeek,
|
|
856
|
+
captionLabels: (_i = this.captions) == null ? void 0 : _i.labels()
|
|
677
857
|
}
|
|
678
858
|
);
|
|
679
859
|
this.root.appendChild(this.controls.el);
|
|
@@ -726,6 +906,7 @@ var Player = class {
|
|
|
726
906
|
this.render();
|
|
727
907
|
}
|
|
728
908
|
onTimeUpdate(current) {
|
|
909
|
+
var _a;
|
|
729
910
|
const advance = current - this.lastTick;
|
|
730
911
|
if (advance > 0 && advance <= NATURAL_ADVANCE_MAX && current > this.furthest) {
|
|
731
912
|
this.furthest = current;
|
|
@@ -733,9 +914,14 @@ var Player = class {
|
|
|
733
914
|
this.provider.seek(this.furthest);
|
|
734
915
|
}
|
|
735
916
|
this.lastTick = current;
|
|
917
|
+
(_a = this.captions) == null ? void 0 : _a.update(current);
|
|
736
918
|
this.emitter.emit("timeupdate", this.snapshot());
|
|
737
919
|
this.render();
|
|
738
920
|
}
|
|
921
|
+
syncCaptions() {
|
|
922
|
+
if (!this.captions) return;
|
|
923
|
+
this.controls.setCaptions(this.captions.getActive(), this.captions.available());
|
|
924
|
+
}
|
|
739
925
|
snapshot() {
|
|
740
926
|
const current = this.provider.getCurrentTime();
|
|
741
927
|
const duration = this.duration || this.provider.getDuration();
|
|
@@ -769,10 +955,12 @@ var Player = class {
|
|
|
769
955
|
else this.provider.play();
|
|
770
956
|
}
|
|
771
957
|
seek(seconds) {
|
|
958
|
+
var _a;
|
|
772
959
|
if (!this.provider.capabilities.seekable) return;
|
|
773
960
|
const target = this.options.lockSeek && seconds > this.furthest ? this.furthest : Math.max(0, seconds);
|
|
774
961
|
this.provider.seek(target);
|
|
775
962
|
this.lastTick = target;
|
|
963
|
+
(_a = this.captions) == null ? void 0 : _a.update(target);
|
|
776
964
|
this.render();
|
|
777
965
|
}
|
|
778
966
|
setLockSeek(locked) {
|
|
@@ -791,9 +979,11 @@ var Player = class {
|
|
|
791
979
|
this.emitter.off(event, cb);
|
|
792
980
|
}
|
|
793
981
|
destroy() {
|
|
982
|
+
var _a;
|
|
794
983
|
for (const dispose of this.disposers) dispose();
|
|
795
984
|
this.disposers = [];
|
|
796
985
|
this.provider.destroy();
|
|
986
|
+
(_a = this.captions) == null ? void 0 : _a.destroy();
|
|
797
987
|
this.controls.destroy();
|
|
798
988
|
this.emitter.clear();
|
|
799
989
|
this.root.remove();
|
|
@@ -806,7 +996,7 @@ function createPlayer(container, options) {
|
|
|
806
996
|
}
|
|
807
997
|
var KodezenPlayer = forwardRef(
|
|
808
998
|
function KodezenPlayer2(props, ref) {
|
|
809
|
-
var _a, _b;
|
|
999
|
+
var _a, _b, _c;
|
|
810
1000
|
const hostRef = useRef(null);
|
|
811
1001
|
const playerRef = useRef(null);
|
|
812
1002
|
const propsRef = useRef(props);
|
|
@@ -856,7 +1046,8 @@ var KodezenPlayer = forwardRef(
|
|
|
856
1046
|
return (_a2 = playerRef.current) == null ? void 0 : _a2.destroy();
|
|
857
1047
|
}
|
|
858
1048
|
}));
|
|
859
|
-
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}`;
|
|
860
1051
|
useEffect(() => {
|
|
861
1052
|
if (!hostRef.current) return;
|
|
862
1053
|
const p = propsRef.current;
|
|
@@ -868,7 +1059,8 @@ var KodezenPlayer = forwardRef(
|
|
|
868
1059
|
muted: p.muted,
|
|
869
1060
|
speeds: p.speeds,
|
|
870
1061
|
controls: p.controls,
|
|
871
|
-
className: p.className
|
|
1062
|
+
className: p.className,
|
|
1063
|
+
tracks: p.tracks
|
|
872
1064
|
});
|
|
873
1065
|
playerRef.current = player;
|
|
874
1066
|
const offs = [
|