@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/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,13 +790,14 @@ 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}` : "");
|
|
@@ -641,6 +812,10 @@ var Player = class {
|
|
|
641
812
|
this.stage.appendChild(hitLayer);
|
|
642
813
|
this.provider = createProvider(options.source);
|
|
643
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
|
+
}
|
|
644
819
|
this.controls = new Controls(
|
|
645
820
|
{
|
|
646
821
|
onToggle: () => this.toggle(),
|
|
@@ -658,17 +833,22 @@ var Player = class {
|
|
|
658
833
|
this.provider.setRate(r);
|
|
659
834
|
this.render();
|
|
660
835
|
},
|
|
836
|
+
onCaptions: (index) => {
|
|
837
|
+
var _a2;
|
|
838
|
+
return (_a2 = this.captions) == null ? void 0 : _a2.select(index);
|
|
839
|
+
},
|
|
661
840
|
onFullscreen: () => this.toggleFullscreen()
|
|
662
841
|
},
|
|
663
842
|
{
|
|
664
843
|
config: {
|
|
665
844
|
...options.controls,
|
|
666
|
-
seek: ((
|
|
667
|
-
speed: ((
|
|
668
|
-
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
|
|
669
848
|
},
|
|
670
849
|
speeds,
|
|
671
|
-
lockSeek: !!options.lockSeek
|
|
850
|
+
lockSeek: !!options.lockSeek,
|
|
851
|
+
captionLabels: (_i = this.captions) == null ? void 0 : _i.labels()
|
|
672
852
|
}
|
|
673
853
|
);
|
|
674
854
|
this.root.appendChild(this.controls.el);
|
|
@@ -721,6 +901,7 @@ var Player = class {
|
|
|
721
901
|
this.render();
|
|
722
902
|
}
|
|
723
903
|
onTimeUpdate(current) {
|
|
904
|
+
var _a;
|
|
724
905
|
const advance = current - this.lastTick;
|
|
725
906
|
if (advance > 0 && advance <= NATURAL_ADVANCE_MAX && current > this.furthest) {
|
|
726
907
|
this.furthest = current;
|
|
@@ -728,9 +909,14 @@ var Player = class {
|
|
|
728
909
|
this.provider.seek(this.furthest);
|
|
729
910
|
}
|
|
730
911
|
this.lastTick = current;
|
|
912
|
+
(_a = this.captions) == null ? void 0 : _a.update(current);
|
|
731
913
|
this.emitter.emit("timeupdate", this.snapshot());
|
|
732
914
|
this.render();
|
|
733
915
|
}
|
|
916
|
+
syncCaptions() {
|
|
917
|
+
if (!this.captions) return;
|
|
918
|
+
this.controls.setCaptions(this.captions.getActive(), this.captions.available());
|
|
919
|
+
}
|
|
734
920
|
snapshot() {
|
|
735
921
|
const current = this.provider.getCurrentTime();
|
|
736
922
|
const duration = this.duration || this.provider.getDuration();
|
|
@@ -764,10 +950,12 @@ var Player = class {
|
|
|
764
950
|
else this.provider.play();
|
|
765
951
|
}
|
|
766
952
|
seek(seconds) {
|
|
953
|
+
var _a;
|
|
767
954
|
if (!this.provider.capabilities.seekable) return;
|
|
768
955
|
const target = this.options.lockSeek && seconds > this.furthest ? this.furthest : Math.max(0, seconds);
|
|
769
956
|
this.provider.seek(target);
|
|
770
957
|
this.lastTick = target;
|
|
958
|
+
(_a = this.captions) == null ? void 0 : _a.update(target);
|
|
771
959
|
this.render();
|
|
772
960
|
}
|
|
773
961
|
setLockSeek(locked) {
|
|
@@ -786,9 +974,11 @@ var Player = class {
|
|
|
786
974
|
this.emitter.off(event, cb);
|
|
787
975
|
}
|
|
788
976
|
destroy() {
|
|
977
|
+
var _a;
|
|
789
978
|
for (const dispose of this.disposers) dispose();
|
|
790
979
|
this.disposers = [];
|
|
791
980
|
this.provider.destroy();
|
|
981
|
+
(_a = this.captions) == null ? void 0 : _a.destroy();
|
|
792
982
|
this.controls.destroy();
|
|
793
983
|
this.emitter.clear();
|
|
794
984
|
this.root.remove();
|
|
@@ -800,6 +990,6 @@ function createPlayer(container, options) {
|
|
|
800
990
|
return new Player(container, options);
|
|
801
991
|
}
|
|
802
992
|
|
|
803
|
-
export { Player, createPlayer };
|
|
993
|
+
export { Player, createPlayer, parseVtt };
|
|
804
994
|
//# sourceMappingURL=index.js.map
|
|
805
995
|
//# sourceMappingURL=index.js.map
|