@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.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,13 +797,14 @@ 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}` : "");
|
|
@@ -648,6 +819,10 @@ var Player = class {
|
|
|
648
819
|
this.stage.appendChild(hitLayer);
|
|
649
820
|
this.provider = createProvider(options.source);
|
|
650
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
|
+
}
|
|
651
826
|
this.controls = new Controls(
|
|
652
827
|
{
|
|
653
828
|
onToggle: () => this.toggle(),
|
|
@@ -665,17 +840,22 @@ var Player = class {
|
|
|
665
840
|
this.provider.setRate(r);
|
|
666
841
|
this.render();
|
|
667
842
|
},
|
|
843
|
+
onCaptions: (index) => {
|
|
844
|
+
var _a2;
|
|
845
|
+
return (_a2 = this.captions) == null ? void 0 : _a2.select(index);
|
|
846
|
+
},
|
|
668
847
|
onFullscreen: () => this.toggleFullscreen()
|
|
669
848
|
},
|
|
670
849
|
{
|
|
671
850
|
config: {
|
|
672
851
|
...options.controls,
|
|
673
|
-
seek: ((
|
|
674
|
-
speed: ((
|
|
675
|
-
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
|
|
676
855
|
},
|
|
677
856
|
speeds,
|
|
678
|
-
lockSeek: !!options.lockSeek
|
|
857
|
+
lockSeek: !!options.lockSeek,
|
|
858
|
+
captionLabels: (_i = this.captions) == null ? void 0 : _i.labels()
|
|
679
859
|
}
|
|
680
860
|
);
|
|
681
861
|
this.root.appendChild(this.controls.el);
|
|
@@ -728,6 +908,7 @@ var Player = class {
|
|
|
728
908
|
this.render();
|
|
729
909
|
}
|
|
730
910
|
onTimeUpdate(current) {
|
|
911
|
+
var _a;
|
|
731
912
|
const advance = current - this.lastTick;
|
|
732
913
|
if (advance > 0 && advance <= NATURAL_ADVANCE_MAX && current > this.furthest) {
|
|
733
914
|
this.furthest = current;
|
|
@@ -735,9 +916,14 @@ var Player = class {
|
|
|
735
916
|
this.provider.seek(this.furthest);
|
|
736
917
|
}
|
|
737
918
|
this.lastTick = current;
|
|
919
|
+
(_a = this.captions) == null ? void 0 : _a.update(current);
|
|
738
920
|
this.emitter.emit("timeupdate", this.snapshot());
|
|
739
921
|
this.render();
|
|
740
922
|
}
|
|
923
|
+
syncCaptions() {
|
|
924
|
+
if (!this.captions) return;
|
|
925
|
+
this.controls.setCaptions(this.captions.getActive(), this.captions.available());
|
|
926
|
+
}
|
|
741
927
|
snapshot() {
|
|
742
928
|
const current = this.provider.getCurrentTime();
|
|
743
929
|
const duration = this.duration || this.provider.getDuration();
|
|
@@ -771,10 +957,12 @@ var Player = class {
|
|
|
771
957
|
else this.provider.play();
|
|
772
958
|
}
|
|
773
959
|
seek(seconds) {
|
|
960
|
+
var _a;
|
|
774
961
|
if (!this.provider.capabilities.seekable) return;
|
|
775
962
|
const target = this.options.lockSeek && seconds > this.furthest ? this.furthest : Math.max(0, seconds);
|
|
776
963
|
this.provider.seek(target);
|
|
777
964
|
this.lastTick = target;
|
|
965
|
+
(_a = this.captions) == null ? void 0 : _a.update(target);
|
|
778
966
|
this.render();
|
|
779
967
|
}
|
|
780
968
|
setLockSeek(locked) {
|
|
@@ -793,9 +981,11 @@ var Player = class {
|
|
|
793
981
|
this.emitter.off(event, cb);
|
|
794
982
|
}
|
|
795
983
|
destroy() {
|
|
984
|
+
var _a;
|
|
796
985
|
for (const dispose of this.disposers) dispose();
|
|
797
986
|
this.disposers = [];
|
|
798
987
|
this.provider.destroy();
|
|
988
|
+
(_a = this.captions) == null ? void 0 : _a.destroy();
|
|
799
989
|
this.controls.destroy();
|
|
800
990
|
this.emitter.clear();
|
|
801
991
|
this.root.remove();
|
|
@@ -808,7 +998,7 @@ function createPlayer(container, options) {
|
|
|
808
998
|
}
|
|
809
999
|
var KodezenPlayer = react.forwardRef(
|
|
810
1000
|
function KodezenPlayer2(props, ref) {
|
|
811
|
-
var _a, _b;
|
|
1001
|
+
var _a, _b, _c;
|
|
812
1002
|
const hostRef = react.useRef(null);
|
|
813
1003
|
const playerRef = react.useRef(null);
|
|
814
1004
|
const propsRef = react.useRef(props);
|
|
@@ -858,7 +1048,8 @@ var KodezenPlayer = react.forwardRef(
|
|
|
858
1048
|
return (_a2 = playerRef.current) == null ? void 0 : _a2.destroy();
|
|
859
1049
|
}
|
|
860
1050
|
}));
|
|
861
|
-
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}`;
|
|
862
1053
|
react.useEffect(() => {
|
|
863
1054
|
if (!hostRef.current) return;
|
|
864
1055
|
const p = propsRef.current;
|
|
@@ -870,7 +1061,8 @@ var KodezenPlayer = react.forwardRef(
|
|
|
870
1061
|
muted: p.muted,
|
|
871
1062
|
speeds: p.speeds,
|
|
872
1063
|
controls: p.controls,
|
|
873
|
-
className: p.className
|
|
1064
|
+
className: p.className,
|
|
1065
|
+
tracks: p.tracks
|
|
874
1066
|
});
|
|
875
1067
|
playerRef.current = player;
|
|
876
1068
|
const offs = [
|