@guardvideo/player-sdk 1.2.0 → 2.0.0

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/dist/index.js CHANGED
@@ -659,763 +659,1383 @@ class GuardVideoPlayer {
659
659
  }
660
660
  }
661
661
 
662
- function formatTime(seconds) {
663
- if (!isFinite(seconds) || isNaN(seconds))
662
+ function formatTime(s) {
663
+ if (!isFinite(s) || isNaN(s))
664
664
  return '0:00';
665
- const h = Math.floor(seconds / 3600);
666
- const m = Math.floor((seconds % 3600) / 60);
667
- const s = Math.floor(seconds % 60);
665
+ const h = Math.floor(s / 3600);
666
+ const m = Math.floor((s % 3600) / 60);
667
+ const sc = Math.floor(s % 60);
668
668
  const mm = String(m).padStart(h > 0 ? 2 : 1, '0');
669
- const ss = String(s).padStart(2, '0');
669
+ const ss = String(sc).padStart(2, '0');
670
670
  return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
671
671
  }
672
- const PlayIcon = () => (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor" },
673
- React.createElement("polygon", { points: "5,3 19,12 5,21" })));
674
- const PauseIcon = () => (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor" },
675
- React.createElement("rect", { x: "6", y: "4", width: "4", height: "16", rx: "1" }),
676
- React.createElement("rect", { x: "14", y: "4", width: "4", height: "16", rx: "1" })));
677
- const ReplayIcon = () => (React.createElement("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor" },
678
- React.createElement("path", { d: "M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" })));
679
- const VolumeHighIcon = () => (React.createElement("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor" },
680
- React.createElement("path", { d: "M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" })));
681
- const VolumeMuteIcon = () => (React.createElement("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor" },
682
- React.createElement("path", { d: "M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z" })));
683
- const FullscreenIcon = () => (React.createElement("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor" },
684
- React.createElement("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" })));
685
- const ExitFullscreenIcon = () => (React.createElement("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor" },
686
- React.createElement("path", { d: "M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" })));
687
- const SettingsIcon = () => (React.createElement("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor" },
688
- React.createElement("path", { d: "M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z" })));
689
- const ShieldIcon = ({ color }) => (React.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" },
690
- React.createElement("path", { d: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" })));
672
+ function el(tag, cls, attrs) {
673
+ const e = document.createElement(tag);
674
+ if (cls)
675
+ e.className = cls;
676
+ if (attrs)
677
+ for (const [k, v] of Object.entries(attrs))
678
+ e.setAttribute(k, v);
679
+ return e;
680
+ }
681
+ function svgEl(path, w = 20, h = 20) {
682
+ const s = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
683
+ s.setAttribute('width', String(w));
684
+ s.setAttribute('height', String(h));
685
+ s.setAttribute('viewBox', '0 0 24 24');
686
+ s.setAttribute('fill', 'currentColor');
687
+ s.setAttribute('aria-hidden', 'true');
688
+ s.innerHTML = path;
689
+ return s;
690
+ }
691
+ const ICON = {
692
+ play: '<polygon points="5,3 19,12 5,21"/>',
693
+ pause: '<rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/>',
694
+ replay: '<path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/>',
695
+ volHigh: '<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>',
696
+ volMid: '<path d="M18.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z"/>',
697
+ volLow: '<path d="M7 9v6h4l5 5V4l-5 5H7z"/>',
698
+ volMute: '<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>',
699
+ fsEnter: '<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>',
700
+ fsExit: '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>',
701
+ settings: '<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94zM12,15.6c-1.98,0-3.6-1.62-3.6-3.6s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>',
702
+ shield: '<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'};
703
+ const SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
691
704
  let _stylesInjected = false;
692
- function injectPlayerStyles(accentColor) {
705
+ function injectStyles() {
693
706
  if (_stylesInjected)
694
707
  return;
695
708
  _stylesInjected = true;
696
709
  const css = `
697
- /* ── GuardVideo Custom Player ─────────────────────────────── */
710
+ /* ── GuardVideo Player UI v2 ──────────────────────────────── */
711
+
712
+ /* Google Font import — Outfit for labels, DM Mono for time */
713
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=DM+Mono:wght@400;500&display=swap');
714
+
715
+ /* ── Root ─────────────────────────────────────────────────── */
698
716
  .gvp-root {
717
+ --gvp-accent: #00e5a0;
718
+ --gvp-accent-dim: rgba(0, 229, 160, 0.18);
719
+ --gvp-accent-glow: rgba(0, 229, 160, 0.35);
720
+ --gvp-glass-bg: rgba(8, 8, 14, 0.72);
721
+ --gvp-glass-bdr: rgba(255,255,255,0.07);
722
+ --gvp-text: rgba(255,255,255,0.92);
723
+ --gvp-text-dim: rgba(255,255,255,0.45);
724
+ --gvp-radius: 12px;
725
+ --gvp-font: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
726
+ --gvp-mono: 'DM Mono', 'SF Mono', 'Fira Mono', monospace;
727
+
699
728
  position: relative;
700
- background: #000;
701
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
702
- border-radius: 10px;
729
+ background: #050508;
730
+ font-family: var(--gvp-font);
731
+ border-radius: var(--gvp-radius);
703
732
  overflow: hidden;
704
733
  -webkit-user-select: none;
734
+ -moz-user-select: none;
735
+ -ms-user-select: none;
705
736
  user-select: none;
706
737
  outline: none;
738
+
739
+ /* Subtle inner vignette for cinema depth */
740
+ box-shadow:
741
+ inset 0 0 80px rgba(0,0,0,0.55),
742
+ 0 24px 80px rgba(0,0,0,0.6);
743
+ }
744
+
745
+ .gvp-root:focus-visible {
746
+ outline: 2px solid var(--gvp-accent);
747
+ outline-offset: 2px;
707
748
  }
708
- .gvp-root:focus-visible { box-shadow: 0 0 0 3px ${accentColor}66; }
709
749
 
750
+ /* ── Video ────────────────────────────────────────────────── */
710
751
  .gvp-video {
711
752
  display: block;
712
753
  width: 100%;
713
754
  height: 100%;
755
+ -o-object-fit: contain;
714
756
  object-fit: contain;
715
- border-radius: 10px;
716
757
  }
717
758
 
718
- /* ── Loading spinner ─────────────────────────────────────── */
719
- .gvp-spinner {
759
+ /* ── Utility ──────────────────────────────────────────────── */
760
+ .gvp-hidden { display: none !important; }
761
+ .gvp-controls-hidden { opacity: 0; -webkit-transform: translateY(8px); transform: translateY(8px); pointer-events: none; }
762
+
763
+ /* ── Gradient overlay (top + bottom) ─────────────────────── */
764
+ .gvp-root::before,
765
+ .gvp-root::after {
766
+ content: '';
720
767
  position: absolute;
721
- inset: 0;
722
- display: flex;
723
- align-items: center;
724
- justify-content: center;
768
+ left: 0; right: 0;
725
769
  pointer-events: none;
726
- background: rgba(0,0,0,0.55);
727
- border-radius: 10px;
770
+ z-index: 2;
771
+ }
772
+ .gvp-root::before {
773
+ top: 0;
774
+ height: 90px;
775
+ background: -webkit-linear-gradient(bottom, transparent, rgba(0,0,0,0.55));
776
+ background: linear-gradient(to bottom, rgba(0,0,0,0.55), transparent);
777
+ border-radius: var(--gvp-radius) var(--gvp-radius) 0 0;
778
+ }
779
+ .gvp-root::after {
780
+ bottom: 0;
781
+ height: 160px;
782
+ background: -webkit-linear-gradient(top, transparent, rgba(0,0,0,0.88));
783
+ background: linear-gradient(to bottom, transparent, rgba(0,0,0,0.88));
784
+ border-radius: 0 0 var(--gvp-radius) var(--gvp-radius);
785
+ }
786
+
787
+ /* ── Spinner ──────────────────────────────────────────────── */
788
+ .gvp-spinner {
789
+ position: absolute; inset: 0;
790
+ display: -webkit-box; display: -ms-flexbox; display: flex;
791
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
792
+ -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center;
793
+ pointer-events: none;
794
+ z-index: 8;
728
795
  }
729
796
  .gvp-spinner-ring {
730
- width: 48px;
731
- height: 48px;
732
- border: 3px solid rgba(255,255,255,0.15);
733
- border-top-color: ${accentColor};
797
+ width: 44px; height: 44px;
798
+ border: 2.5px solid rgba(255,255,255,0.08);
799
+ border-top-color: var(--gvp-accent);
734
800
  border-radius: 50%;
735
- animation: gvp-spin 0.8s linear infinite;
801
+ -webkit-animation: gvp-spin 0.75s linear infinite;
802
+ animation: gvp-spin 0.75s linear infinite;
803
+ box-shadow: 0 0 16px var(--gvp-accent-glow);
736
804
  }
737
- @keyframes gvp-spin { to { transform: rotate(360deg); } }
805
+ @-webkit-keyframes gvp-spin { to { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }
806
+ @keyframes gvp-spin { to { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }
738
807
 
739
- /* ── Big centre play button (initial state) ───────────────── */
808
+ /* ── Centre play overlay ──────────────────────────────────── */
740
809
  .gvp-center-play {
741
- position: absolute;
742
- inset: 0;
743
- display: flex;
744
- align-items: center;
745
- justify-content: center;
810
+ position: absolute; inset: 0; z-index: 6;
811
+ display: -webkit-box; display: -ms-flexbox; display: flex;
812
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
813
+ -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center;
746
814
  cursor: pointer;
747
- background: rgba(0,0,0,0.3);
748
- transition: background 0.2s;
749
- border-radius: 10px;
815
+ background: transparent;
816
+ -webkit-transition: background 0.25s; transition: background 0.25s;
817
+ border-radius: var(--gvp-radius);
750
818
  }
751
- .gvp-center-play:hover { background: rgba(0,0,0,0.45); }
819
+ .gvp-center-play:hover { background: rgba(0,0,0,0.22); }
820
+
752
821
  .gvp-center-play-btn {
753
- width: 72px;
754
- height: 72px;
755
- background: rgba(255,255,255,0.12);
756
- backdrop-filter: blur(8px);
822
+ position: relative;
823
+ width: 76px; height: 76px;
757
824
  border-radius: 50%;
758
- display: flex;
759
- align-items: center;
760
- justify-content: center;
761
- border: 2px solid rgba(255,255,255,0.25);
762
- transition: background 0.2s, transform 0.15s;
825
+ display: -webkit-box; display: -ms-flexbox; display: flex;
826
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
827
+ -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center;
763
828
  color: #fff;
829
+ /* Glass morphism */
830
+ background: rgba(255,255,255,0.10);
831
+ -webkit-backdrop-filter: blur(16px) saturate(180%);
832
+ backdrop-filter: blur(16px) saturate(180%);
833
+ border: 1px solid rgba(255,255,255,0.18);
834
+ -webkit-transition: background 0.2s, -webkit-transform 0.2s, -webkit-box-shadow 0.2s;
835
+ transition: background 0.2s, transform 0.2s, box-shadow 0.2s;
836
+ box-shadow: 0 4px 32px rgba(0,0,0,0.45), 0 0 0 0 var(--gvp-accent-glow);
837
+ }
838
+ /* Ripple ring on hover */
839
+ .gvp-center-play-btn::after {
840
+ content: '';
841
+ position: absolute; inset: -4px;
842
+ border-radius: 50%;
843
+ border: 1.5px solid rgba(255,255,255,0.12);
844
+ -webkit-transition: border-color 0.2s, opacity 0.2s; transition: border-color 0.2s, opacity 0.2s;
845
+ opacity: 0;
846
+ }
847
+ .gvp-center-play:hover .gvp-center-play-btn::after {
848
+ opacity: 1;
849
+ border-color: var(--gvp-accent);
764
850
  }
765
851
  .gvp-center-play:hover .gvp-center-play-btn {
766
- background: ${accentColor}cc;
767
- transform: scale(1.08);
852
+ background: var(--gvp-accent-dim);
853
+ -webkit-transform: scale(1.07);
854
+ transform: scale(1.07);
855
+ box-shadow: 0 4px 40px rgba(0,0,0,0.55), 0 0 28px var(--gvp-accent-glow);
768
856
  }
769
857
 
770
858
  /* ── Click-to-toggle overlay ─────────────────────────────── */
771
859
  .gvp-click-area {
772
- position: absolute;
773
- inset: 0;
774
- cursor: pointer;
775
- z-index: 1;
860
+ position: absolute; inset: 0;
861
+ cursor: pointer; z-index: 4;
776
862
  }
777
863
 
778
- /* ── Ripple effect on click ──────────────────────────────── */
864
+ /* ── Ripple animation ─────────────────────────────────────── */
779
865
  .gvp-ripple {
780
- position: absolute;
781
- border-radius: 50%;
782
- transform: scale(0);
783
- background: rgba(255,255,255,0.25);
784
- animation: gvp-ripple-anim 0.5s ease-out forwards;
785
- pointer-events: none;
786
- z-index: 2;
866
+ position: absolute; border-radius: 50%;
867
+ -webkit-transform: scale(0); transform: scale(0);
868
+ background: rgba(255,255,255,0.18);
869
+ -webkit-animation: gvp-ripple-anim 0.55s cubic-bezier(0.22,1,0.36,1) forwards;
870
+ animation: gvp-ripple-anim 0.55s cubic-bezier(0.22,1,0.36,1) forwards;
871
+ pointer-events: none; z-index: 5;
787
872
  }
788
- @keyframes gvp-ripple-anim { to { transform: scale(4); opacity: 0; } }
873
+ @-webkit-keyframes gvp-ripple-anim { to { -webkit-transform: scale(5); transform: scale(5); opacity: 0; } }
874
+ @keyframes gvp-ripple-anim { to { -webkit-transform: scale(5); transform: scale(5); opacity: 0; } }
789
875
 
790
- /* ── Controls bar ────────────────────────────────────────── */
876
+ /* ── Controls bar ─────────────────────────────────────────── */
791
877
  .gvp-controls {
792
878
  position: absolute;
793
- bottom: 0;
794
- left: 0;
795
- right: 0;
879
+ bottom: 0; left: 0; right: 0;
796
880
  z-index: 10;
797
- background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, transparent 100%);
798
- padding: 36px 14px 10px;
799
- transition: opacity 0.3s, transform 0.3s;
800
- border-radius: 0 0 10px 10px;
881
+ padding: 0 14px 14px;
882
+ -webkit-transition: opacity 0.35s cubic-bezier(0.4,0,0.2,1),
883
+ -webkit-transform 0.35s cubic-bezier(0.4,0,0.2,1);
884
+ transition: opacity 0.35s cubic-bezier(0.4,0,0.2,1),
885
+ transform 0.35s cubic-bezier(0.4,0,0.2,1);
886
+ border-radius: 0 0 var(--gvp-radius) var(--gvp-radius);
801
887
  }
802
- .gvp-controls-hidden {
803
- opacity: 0;
804
- transform: translateY(6px);
805
- pointer-events: none;
888
+
889
+ /* Inner glass pill that wraps all controls */
890
+ .gvp-controls-inner {
891
+ background: var(--gvp-glass-bg);
892
+ -webkit-backdrop-filter: blur(24px) saturate(160%);
893
+ backdrop-filter: blur(24px) saturate(160%);
894
+ border: 1px solid var(--gvp-glass-bdr);
895
+ border-radius: 10px;
896
+ padding: 10px 12px 10px;
897
+ -webkit-box-shadow: 0 8px 32px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.05);
898
+ box-shadow: 0 8px 32px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.05);
806
899
  }
807
900
 
808
- /* ── Progress / seek bar ─────────────────────────────────── */
901
+ /* ── Seek row ─────────────────────────────────────────────── */
809
902
  .gvp-seek-row {
810
- display: flex;
811
- align-items: center;
903
+ display: -webkit-box; display: -ms-flexbox; display: flex;
904
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
812
905
  gap: 8px;
813
- margin-bottom: 6px;
906
+ margin-bottom: 8px;
814
907
  }
908
+
815
909
  .gvp-seek-wrap {
816
- flex: 1;
910
+ -webkit-box-flex: 1; -ms-flex: 1; flex: 1;
817
911
  position: relative;
818
912
  height: 4px;
819
913
  cursor: pointer;
820
914
  padding: 8px 0;
821
915
  margin: -8px 0;
822
- box-sizing: content-box;
916
+ -webkit-box-sizing: content-box; box-sizing: content-box;
917
+ touch-action: none;
823
918
  }
919
+
824
920
  .gvp-seek-track {
825
- height: 4px;
826
- background: rgba(255,255,255,0.2);
827
- border-radius: 4px;
921
+ height: 3px;
922
+ background: rgba(255,255,255,0.12);
923
+ border-radius: 99px;
828
924
  position: relative;
829
- overflow: hidden;
830
- transition: height 0.15s;
925
+ overflow: visible;
926
+ -webkit-transition: height 0.15s; transition: height 0.15s;
927
+ pointer-events: none;
831
928
  }
832
- .gvp-seek-wrap:hover .gvp-seek-track { height: 6px; }
929
+ .gvp-seek-wrap:hover .gvp-seek-track,
930
+ .gvp-seek-wrap.gvp-dragging .gvp-seek-track { height: 5px; }
931
+
833
932
  .gvp-seek-buffered {
834
- position: absolute;
835
- left: 0;
836
- top: 0;
837
- height: 100%;
838
- background: rgba(255,255,255,0.35);
839
- border-radius: 4px;
933
+ position: absolute; left: 0; top: 0; height: 100%;
934
+ background: rgba(255,255,255,0.22);
935
+ border-radius: 99px;
840
936
  pointer-events: none;
937
+ -webkit-transition: width 0.3s; transition: width 0.3s;
841
938
  }
939
+
842
940
  .gvp-seek-progress {
843
- position: absolute;
844
- left: 0;
845
- top: 0;
846
- height: 100%;
847
- background: ${accentColor};
848
- border-radius: 4px;
941
+ position: absolute; left: 0; top: 0; height: 100%;
942
+ background: -webkit-linear-gradient(left, var(--gvp-accent), color-mix(in srgb, var(--gvp-accent) 80%, #fff 20%));
943
+ background: linear-gradient(to right, var(--gvp-accent), color-mix(in srgb, var(--gvp-accent) 80%, #fff 20%));
944
+ border-radius: 99px;
849
945
  pointer-events: none;
850
- transition: width 0.1s linear;
946
+ /* Glow on the progress fill */
947
+ -webkit-box-shadow: 0 0 8px var(--gvp-accent-glow);
948
+ box-shadow: 0 0 8px var(--gvp-accent-glow);
851
949
  }
950
+
951
+ /* Thumb — visible on hover/drag */
852
952
  .gvp-seek-thumb {
853
- position: absolute;
854
- top: 50%;
855
- transform: translate(-50%, -50%);
856
- width: 13px;
857
- height: 13px;
953
+ position: absolute; top: 50%;
954
+ -webkit-transform: translate(-50%, -50%) scale(0);
955
+ transform: translate(-50%, -50%) scale(0);
956
+ width: 14px; height: 14px;
858
957
  background: #fff;
859
958
  border-radius: 50%;
860
- box-shadow: 0 1px 4px rgba(0,0,0,0.5);
861
959
  pointer-events: none;
862
- opacity: 0;
863
- transition: opacity 0.15s;
960
+ -webkit-box-shadow: 0 0 0 3px var(--gvp-accent-dim), 0 2px 6px rgba(0,0,0,0.5);
961
+ box-shadow: 0 0 0 3px var(--gvp-accent-dim), 0 2px 6px rgba(0,0,0,0.5);
962
+ -webkit-transition: -webkit-transform 0.15s cubic-bezier(0.34,1.56,0.64,1);
963
+ transition: transform 0.15s cubic-bezier(0.34,1.56,0.64,1);
964
+ }
965
+ .gvp-seek-wrap:hover .gvp-seek-thumb,
966
+ .gvp-seek-wrap.gvp-dragging .gvp-seek-thumb {
967
+ -webkit-transform: translate(-50%,-50%) scale(1);
968
+ transform: translate(-50%,-50%) scale(1);
864
969
  }
865
- .gvp-seek-wrap:hover .gvp-seek-thumb { opacity: 1; }
866
970
 
867
- /* ── Time tooltip ─────────────────────────────────────────── */
971
+ /* Seek tooltip */
868
972
  .gvp-seek-tooltip {
869
973
  position: absolute;
870
- bottom: 24px;
871
- transform: translateX(-50%);
872
- background: rgba(0,0,0,0.8);
873
- color: #fff;
974
+ bottom: 26px;
975
+ -webkit-transform: translateX(-50%);
976
+ transform: translateX(-50%);
977
+ background: rgba(10,10,14,0.92);
978
+ -webkit-backdrop-filter: blur(8px);
979
+ backdrop-filter: blur(8px);
980
+ border: 1px solid var(--gvp-glass-bdr);
981
+ color: var(--gvp-text);
982
+ font-family: var(--gvp-mono);
874
983
  font-size: 11px;
875
984
  font-weight: 500;
876
- padding: 3px 7px;
877
- border-radius: 4px;
985
+ padding: 3px 8px;
986
+ border-radius: 5px;
878
987
  pointer-events: none;
879
988
  white-space: nowrap;
880
989
  opacity: 0;
881
- transition: opacity 0.1s;
990
+ -webkit-transition: opacity 0.12s; transition: opacity 0.12s;
991
+ -webkit-box-shadow: 0 4px 12px rgba(0,0,0,0.45); box-shadow: 0 4px 12px rgba(0,0,0,0.45);
882
992
  }
883
993
  .gvp-seek-wrap:hover .gvp-seek-tooltip { opacity: 1; }
884
994
 
885
- /* ── Bottom control row ───────────────────────────────────── */
995
+ /* Caret below tooltip */
996
+ .gvp-seek-tooltip::after {
997
+ content: '';
998
+ position: absolute; bottom: -5px; left: 50%;
999
+ -webkit-transform: translateX(-50%); transform: translateX(-50%);
1000
+ border: 4px solid transparent;
1001
+ border-top-color: rgba(10,10,14,0.92);
1002
+ border-bottom-width: 0;
1003
+ }
1004
+
1005
+ /* ── Button row ───────────────────────────────────────────── */
886
1006
  .gvp-btn-row {
887
- display: flex;
888
- align-items: center;
889
- gap: 4px;
1007
+ display: -webkit-box; display: -ms-flexbox; display: flex;
1008
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
1009
+ gap: 2px;
890
1010
  }
1011
+
891
1012
  .gvp-btn {
892
- background: none;
893
- border: none;
894
- color: #fff;
1013
+ background: none; border: none;
1014
+ color: rgba(255,255,255,0.75);
895
1015
  cursor: pointer;
896
- padding: 5px;
897
- border-radius: 6px;
898
- display: flex;
899
- align-items: center;
900
- justify-content: center;
901
- transition: background 0.15s, color 0.15s;
902
- flex-shrink: 0;
1016
+ padding: 6px;
1017
+ border-radius: 7px;
1018
+ display: -webkit-inline-box; display: -ms-inline-flexbox; display: inline-flex;
1019
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
1020
+ -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center;
1021
+ -webkit-transition: background 0.14s, color 0.14s, -webkit-transform 0.12s;
1022
+ transition: background 0.14s, color 0.14s, transform 0.12s;
1023
+ -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0;
1024
+ line-height: 0;
1025
+ }
1026
+ .gvp-btn:hover {
1027
+ background: rgba(255,255,255,0.09);
1028
+ color: #fff;
1029
+ -webkit-transform: scale(1.08); transform: scale(1.08);
1030
+ }
1031
+ .gvp-btn:active {
1032
+ background: rgba(255,255,255,0.14);
1033
+ -webkit-transform: scale(0.96); transform: scale(0.96);
903
1034
  }
904
- .gvp-btn:hover { background: rgba(255,255,255,0.12); color: ${accentColor}; }
905
- .gvp-btn:active { background: rgba(255,255,255,0.2); }
1035
+ /* Accent highlight on play button */
1036
+ .gvp-btn-play:hover { background: var(--gvp-accent-dim); color: var(--gvp-accent); }
906
1037
 
907
- /* ── Volume section ──────────────────────────────────────── */
1038
+ /* ── Volume group ─────────────────────────────────────────── */
908
1039
  .gvp-volume-wrap {
909
- display: flex;
910
- align-items: center;
911
- gap: 5px;
1040
+ display: -webkit-box; display: -ms-flexbox; display: flex;
1041
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
1042
+ gap: 2px;
912
1043
  }
913
1044
  .gvp-volume-slider {
914
1045
  -webkit-appearance: none;
915
- width: 70px;
916
- height: 4px;
917
- background: rgba(255,255,255,0.25);
918
- border-radius: 4px;
1046
+ -moz-appearance: none;
1047
+ appearance: none;
1048
+ width: 0;
1049
+ max-width: 72px;
1050
+ height: 3px;
1051
+ background: rgba(255,255,255,0.18);
1052
+ border-radius: 99px;
919
1053
  outline: none;
920
1054
  cursor: pointer;
921
- accent-color: ${accentColor};
922
- transition: width 0.2s;
1055
+ overflow: visible;
1056
+ -webkit-transition: width 0.22s cubic-bezier(0.4,0,0.2,1), opacity 0.22s;
1057
+ transition: width 0.22s cubic-bezier(0.4,0,0.2,1), opacity 0.22s;
1058
+ opacity: 0;
1059
+ /* accent-color is supported in modern browsers */
1060
+ accent-color: var(--gvp-accent);
923
1061
  }
1062
+ /* Show slider when volume-wrap is hovered */
1063
+ .gvp-volume-wrap:hover .gvp-volume-slider,
1064
+ .gvp-volume-wrap:focus-within .gvp-volume-slider {
1065
+ width: 72px;
1066
+ opacity: 1;
1067
+ }
1068
+ /* WebKit thumb */
924
1069
  .gvp-volume-slider::-webkit-slider-thumb {
925
1070
  -webkit-appearance: none;
926
1071
  width: 12px; height: 12px;
927
1072
  border-radius: 50%;
928
1073
  background: #fff;
929
1074
  cursor: pointer;
930
- box-shadow: 0 1px 3px rgba(0,0,0,0.4);
1075
+ -webkit-box-shadow: 0 1px 4px rgba(0,0,0,0.4);
1076
+ box-shadow: 0 1px 4px rgba(0,0,0,0.4);
931
1077
  }
1078
+ /* Firefox thumb */
932
1079
  .gvp-volume-slider::-moz-range-thumb {
933
1080
  width: 12px; height: 12px;
934
1081
  border-radius: 50%;
935
1082
  background: #fff;
936
1083
  cursor: pointer;
937
1084
  border: none;
1085
+ box-shadow: 0 1px 4px rgba(0,0,0,0.4);
1086
+ }
1087
+ /* Firefox track */
1088
+ .gvp-volume-slider::-moz-range-track {
1089
+ background: rgba(255,255,255,0.18);
1090
+ border-radius: 99px;
1091
+ height: 3px;
1092
+ }
1093
+ /* Edge/IE thumb */
1094
+ .gvp-volume-slider::-ms-thumb {
1095
+ width: 12px; height: 12px;
1096
+ border-radius: 50%;
1097
+ background: #fff;
1098
+ cursor: pointer;
1099
+ border: none;
938
1100
  }
1101
+ .gvp-volume-slider::-ms-track {
1102
+ background: rgba(255,255,255,0.18);
1103
+ border-color: transparent;
1104
+ color: transparent;
1105
+ height: 3px;
1106
+ }
1107
+ .gvp-volume-slider::-ms-fill-lower { background: var(--gvp-accent); border-radius: 99px; }
939
1108
 
940
- /* ── Time display ─────────────────────────────────────────── */
1109
+ /* ── Time display ──────────────────────────────────────────── */
941
1110
  .gvp-time {
1111
+ font-family: var(--gvp-mono);
942
1112
  font-size: 12px;
943
- color: rgba(255,255,255,0.85);
944
- font-variant-numeric: tabular-nums;
1113
+ font-weight: 500;
1114
+ color: var(--gvp-text);
945
1115
  white-space: nowrap;
946
- letter-spacing: 0.02em;
1116
+ letter-spacing: 0.04em;
1117
+ padding: 0 4px;
1118
+ /* Tabular numerals so digits don't shift width */
1119
+ font-variant-numeric: tabular-nums;
1120
+ -moz-font-feature-settings: "tnum";
1121
+ -webkit-font-feature-settings: "tnum";
1122
+ font-feature-settings: "tnum";
947
1123
  }
1124
+ .gvp-time-sep { color: var(--gvp-text-dim); margin: 0 2px; }
948
1125
 
949
- /* ── Spacer ──────────────────────────────────────────────── */
950
- .gvp-spacer { flex: 1; }
1126
+ .gvp-spacer { -webkit-box-flex: 1; -ms-flex: 1; flex: 1; }
951
1127
 
952
- /* ── Quality / Settings menu ──────────────────────────────── */
953
- .gvp-menu-wrap {
954
- position: relative;
1128
+ /* ── Speed button ─────────────────────────────────────────── */
1129
+ .gvp-rate-label {
1130
+ font-family: var(--gvp-font);
1131
+ font-size: 11px; font-weight: 700;
1132
+ color: var(--gvp-text-dim);
1133
+ min-width: 30px; text-align: center;
1134
+ cursor: pointer;
1135
+ padding: 5px 4px;
1136
+ border-radius: 6px;
1137
+ -webkit-transition: background 0.14s, color 0.14s; transition: background 0.14s, color 0.14s;
1138
+ -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0;
1139
+ letter-spacing: 0.01em;
1140
+ line-height: 1;
955
1141
  }
1142
+ .gvp-rate-label:hover { background: rgba(255,255,255,0.09); color: #fff; }
1143
+ .gvp-rate-label-active { color: var(--gvp-accent); }
1144
+
1145
+ /* ── Popup menus ──────────────────────────────────────────── */
1146
+ .gvp-menu-wrap { position: relative; }
1147
+
956
1148
  .gvp-menu {
957
1149
  position: absolute;
958
- bottom: calc(100% + 8px);
1150
+ bottom: calc(100% + 10px);
959
1151
  right: 0;
960
- background: rgba(18,18,22,0.95);
961
- backdrop-filter: blur(12px);
962
- -webkit-backdrop-filter: blur(12px);
963
- border: 1px solid rgba(255,255,255,0.08);
964
- border-radius: 8px;
965
- min-width: 140px;
1152
+ background: rgba(12,12,18,0.96);
1153
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
1154
+ backdrop-filter: blur(20px) saturate(180%);
1155
+ border: 1px solid var(--gvp-glass-bdr);
1156
+ border-radius: 10px;
1157
+ min-width: 148px;
966
1158
  overflow: hidden;
967
- box-shadow: 0 8px 24px rgba(0,0,0,0.5);
968
- animation: gvp-menu-in 0.1s ease-out;
1159
+ -webkit-box-shadow: 0 12px 40px rgba(0,0,0,0.65), 0 0 0 0.5px rgba(255,255,255,0.04);
1160
+ box-shadow: 0 12px 40px rgba(0,0,0,0.65), 0 0 0 0.5px rgba(255,255,255,0.04);
1161
+ -webkit-animation: gvp-menu-in 0.14s cubic-bezier(0.22,1,0.36,1);
1162
+ animation: gvp-menu-in 0.14s cubic-bezier(0.22,1,0.36,1);
969
1163
  z-index: 20;
970
1164
  }
971
- @keyframes gvp-menu-in { from { opacity:0; transform: scale(0.95) translateY(4px); } to { opacity:1; transform: scale(1) translateY(0); } }
1165
+ @-webkit-keyframes gvp-menu-in {
1166
+ from { opacity: 0; -webkit-transform: scale(0.92) translateY(6px); transform: scale(0.92) translateY(6px); }
1167
+ to { opacity: 1; -webkit-transform: none; transform: none; }
1168
+ }
1169
+ @keyframes gvp-menu-in {
1170
+ from { opacity: 0; -webkit-transform: scale(0.92) translateY(6px); transform: scale(0.92) translateY(6px); }
1171
+ to { opacity: 1; -webkit-transform: none; transform: none; }
1172
+ }
1173
+
972
1174
  .gvp-menu-title {
973
- font-size: 10px;
974
- font-weight: 600;
1175
+ font-family: var(--gvp-font);
1176
+ font-size: 10px; font-weight: 700;
975
1177
  text-transform: uppercase;
976
- letter-spacing: 0.08em;
977
- color: rgba(255,255,255,0.4);
978
- padding: 8px 12px 4px;
1178
+ letter-spacing: 0.12em;
1179
+ color: var(--gvp-text-dim);
1180
+ padding: 10px 13px 5px;
979
1181
  }
1182
+
980
1183
  .gvp-menu-item {
981
- display: flex;
982
- align-items: center;
983
- justify-content: space-between;
984
- padding: 7px 12px;
985
- font-size: 13px;
986
- color: rgba(255,255,255,0.85);
1184
+ display: -webkit-box; display: -ms-flexbox; display: flex;
1185
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
1186
+ -webkit-box-pack: justify; -ms-flex-pack: justify; justify-content: space-between;
1187
+ padding: 7px 13px;
1188
+ font-family: var(--gvp-font);
1189
+ font-size: 13px; font-weight: 500;
1190
+ color: var(--gvp-text);
987
1191
  cursor: pointer;
988
- transition: background 0.12s;
989
- gap: 10px;
1192
+ -webkit-transition: background 0.1s; transition: background 0.1s;
1193
+ gap: 12px;
1194
+ }
1195
+ .gvp-menu-item:hover { background: rgba(255,255,255,0.06); }
1196
+ .gvp-menu-item-active { color: var(--gvp-accent); }
1197
+
1198
+ .gvp-menu-check {
1199
+ font-size: 13px;
1200
+ color: var(--gvp-accent);
1201
+ line-height: 1;
1202
+ /* Unicode checkmark — crisp on all platforms */
1203
+ }
1204
+
1205
+ .gvp-menu-sep {
1206
+ height: 1px;
1207
+ background: rgba(255,255,255,0.06);
1208
+ margin: 3px 0;
990
1209
  }
991
- .gvp-menu-item:hover { background: rgba(255,255,255,0.07); }
992
- .gvp-menu-item-active { color: ${accentColor}; font-weight: 600; }
993
- .gvp-menu-check { font-size: 14px; color: ${accentColor}; }
994
- .gvp-menu-sep { height: 1px; background: rgba(255,255,255,0.07); margin: 3px 0; }
995
1210
 
996
1211
  /* ── Error overlay ────────────────────────────────────────── */
997
1212
  .gvp-error {
998
- position: absolute;
999
- inset: 0;
1000
- display: flex;
1001
- flex-direction: column;
1002
- align-items: center;
1003
- justify-content: center;
1004
- background: rgba(0,0,0,0.8);
1213
+ position: absolute; inset: 0; z-index: 15;
1214
+ display: -webkit-box; display: -ms-flexbox; display: flex;
1215
+ -webkit-box-orient: vertical; -webkit-box-direction: normal;
1216
+ -ms-flex-direction: column; flex-direction: column;
1217
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
1218
+ -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center;
1219
+ background: rgba(6,6,10,0.88);
1220
+ -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
1005
1221
  color: #fff;
1006
1222
  gap: 10px;
1007
- padding: 24px;
1008
- border-radius: 10px;
1223
+ padding: 28px;
1224
+ border-radius: var(--gvp-radius);
1009
1225
  text-align: center;
1010
1226
  }
1011
- .gvp-error-icon { font-size: 36px; }
1012
- .gvp-error-code { font-size: 11px; font-weight: 700; letter-spacing: 0.1em; color: #f87171; text-transform: uppercase; }
1013
- .gvp-error-msg { font-size: 14px; color: rgba(255,255,255,0.7); max-width: 320px; line-height: 1.5; }
1227
+ .gvp-error-icon { font-size: 40px; line-height: 1; }
1228
+ .gvp-error-code {
1229
+ font-family: var(--gvp-mono);
1230
+ font-size: 10px; font-weight: 500;
1231
+ letter-spacing: 0.14em;
1232
+ color: #f87171;
1233
+ text-transform: uppercase;
1234
+ background: rgba(248,113,113,0.1);
1235
+ border: 1px solid rgba(248,113,113,0.25);
1236
+ padding: 3px 10px; border-radius: 4px;
1237
+ }
1238
+ .gvp-error-msg {
1239
+ font-family: var(--gvp-font);
1240
+ font-size: 14px; font-weight: 400;
1241
+ color: rgba(255,255,255,0.6);
1242
+ max-width: 320px; line-height: 1.6;
1243
+ }
1014
1244
 
1015
1245
  /* ── Secure badge ─────────────────────────────────────────── */
1016
1246
  .gvp-badge {
1017
- position: absolute;
1018
- top: 10px;
1019
- right: 12px;
1020
- display: flex;
1021
- align-items: center;
1247
+ position: absolute; top: 12px; right: 14px; z-index: 5;
1248
+ display: -webkit-inline-box; display: -ms-inline-flexbox; display: inline-flex;
1249
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
1022
1250
  gap: 5px;
1023
- background: rgba(0,0,0,0.55);
1024
- backdrop-filter: blur(6px);
1025
- border-radius: 20px;
1026
- padding: 3px 9px 3px 7px;
1027
- font-size: 10px;
1028
- font-weight: 600;
1029
- color: ${accentColor};
1251
+ background: rgba(8,8,14,0.60);
1252
+ -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
1253
+ border: 1px solid rgba(255,255,255,0.08);
1254
+ border-radius: 99px;
1255
+ padding: 4px 10px 4px 7px;
1256
+ font-family: var(--gvp-font);
1257
+ font-size: 10px; font-weight: 700;
1258
+ color: var(--gvp-accent);
1030
1259
  pointer-events: none;
1031
- z-index: 5;
1032
- letter-spacing: 0.04em;
1033
- transition: opacity 0.3s;
1260
+ letter-spacing: 0.06em;
1261
+ text-transform: uppercase;
1262
+ -webkit-transition: opacity 0.3s; transition: opacity 0.3s;
1263
+ -webkit-box-shadow: 0 2px 12px rgba(0,0,0,0.4), 0 0 0 0.5px rgba(255,255,255,0.05);
1264
+ box-shadow: 0 2px 12px rgba(0,0,0,0.4), 0 0 0 0.5px rgba(255,255,255,0.05);
1034
1265
  }
1035
1266
  .gvp-badge-hidden { opacity: 0; }
1036
1267
 
1037
- /* ── Forensic watermark strip ─────────────────────────────── */
1268
+ /* ── Forensic watermark ───────────────────────────────────── */
1038
1269
  .gvp-watermark {
1270
+ position: absolute; inset: 0;
1271
+ pointer-events: none; overflow: hidden; z-index: 6;
1272
+ }
1273
+ .gvp-watermark-text {
1039
1274
  position: absolute;
1040
- inset: 0;
1275
+ white-space: nowrap;
1276
+ font-family: var(--gvp-mono);
1277
+ font-size: 12px;
1278
+ color: rgba(255,255,255,0.055);
1279
+ -webkit-transform: rotate(-28deg); transform: rotate(-28deg);
1280
+ -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none;
1041
1281
  pointer-events: none;
1042
- overflow: hidden;
1043
- z-index: 6;
1282
+ letter-spacing: 0.06em;
1044
1283
  }
1045
- .gvp-watermark-text {
1284
+
1285
+ /* ── Live dot (for live streams) ─────────────────────────── */
1286
+ .gvp-live-badge {
1287
+ display: -webkit-inline-box; display: -ms-inline-flexbox; display: inline-flex;
1288
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
1289
+ gap: 5px;
1290
+ font-family: var(--gvp-font);
1291
+ font-size: 11px; font-weight: 700;
1292
+ color: #f87171;
1293
+ letter-spacing: 0.08em;
1294
+ text-transform: uppercase;
1295
+ }
1296
+ .gvp-live-dot {
1297
+ width: 7px; height: 7px;
1298
+ background: #f87171;
1299
+ border-radius: 50%;
1300
+ -webkit-animation: gvp-live-pulse 1.5s ease-in-out infinite;
1301
+ animation: gvp-live-pulse 1.5s ease-in-out infinite;
1302
+ }
1303
+ @-webkit-keyframes gvp-live-pulse {
1304
+ 0%, 100% { opacity: 1; -webkit-transform: scale(1); transform: scale(1); }
1305
+ 50% { opacity: 0.5; -webkit-transform: scale(0.7); transform: scale(0.7); }
1306
+ }
1307
+ @keyframes gvp-live-pulse {
1308
+ 0%, 100% { opacity: 1; transform: scale(1); }
1309
+ 50% { opacity: 0.5; transform: scale(0.7); }
1310
+ }
1311
+
1312
+ /* ── Divider between button groups ───────────────────────── */
1313
+ .gvp-divider {
1314
+ width: 1px; height: 18px;
1315
+ background: rgba(255,255,255,0.1);
1316
+ margin: 0 4px;
1317
+ -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0;
1318
+ }
1319
+
1320
+ /* ── Tooltip on buttons ───────────────────────────────────── */
1321
+ .gvp-btn[title]:hover::after {
1322
+ content: attr(title);
1046
1323
  position: absolute;
1324
+ bottom: calc(100% + 7px);
1325
+ left: 50%; -webkit-transform: translateX(-50%); transform: translateX(-50%);
1326
+ background: rgba(10,10,14,0.95);
1327
+ border: 1px solid var(--gvp-glass-bdr);
1328
+ color: var(--gvp-text);
1329
+ font-family: var(--gvp-font);
1330
+ font-size: 11px; font-weight: 500;
1331
+ padding: 3px 8px; border-radius: 5px;
1047
1332
  white-space: nowrap;
1048
- font-size: 13px;
1049
- font-family: monospace;
1050
- color: rgba(255,255,255,0.065);
1051
- transform: rotate(-28deg);
1052
- user-select: none;
1053
1333
  pointer-events: none;
1054
- letter-spacing: 0.05em;
1334
+ z-index: 30;
1055
1335
  }
1056
1336
 
1057
- /* ── Playback rate menu ────────────────────────────────────── */
1058
- .gvp-rate-label {
1059
- font-size: 11px;
1060
- font-weight: 700;
1061
- color: rgba(255,255,255,0.7);
1062
- min-width: 28px;
1063
- text-align: center;
1064
- cursor: pointer;
1065
- padding: 5px 4px;
1066
- border-radius: 4px;
1067
- transition: background 0.12s;
1068
- flex-shrink: 0;
1337
+ /* ── Focus ring (keyboard nav) ────────────────────────────── */
1338
+ .gvp-btn:focus-visible,
1339
+ .gvp-rate-label:focus-visible,
1340
+ .gvp-seek-wrap:focus-visible {
1341
+ outline: 2px solid var(--gvp-accent);
1342
+ outline-offset: 2px;
1343
+ }
1344
+
1345
+ /* ── Responsive: hide volume slider text on tiny players ─── */
1346
+ @media (max-width: 380px) {
1347
+ .gvp-volume-slider { display: none; }
1348
+ .gvp-time { font-size: 11px; }
1349
+ .gvp-controls-inner { padding: 8px 10px; }
1069
1350
  }
1070
- .gvp-rate-label:hover { background: rgba(255,255,255,0.1); }
1071
1351
  `;
1072
1352
  const tag = document.createElement('style');
1073
- tag.setAttribute('data-guardvideo', 'player-ui-styles');
1353
+ tag.setAttribute('data-guardvideo', 'player-ui-styles-v2');
1074
1354
  tag.textContent = css;
1075
1355
  document.head.appendChild(tag);
1076
1356
  }
1077
- const GuardVideoPlayerComponent = React.forwardRef((props, ref) => {
1078
- const { videoId, width = '100%', height = 'auto', embedTokenEndpoint, apiBaseUrl, debug = false, autoplay = false, className = '', style = {}, hlsConfig, branding, contextMenuItems, security, viewerName, viewerEmail, forensicWatermark = true, onReady, onError, onQualityChange, onStateChange, onTimeUpdate: onTimeUpdateProp, onEnded, } = props;
1079
- const accentColor = branding?.accentColor ?? '#44c09b';
1080
- const brandName = branding?.name ?? 'GuardVideo';
1081
- const containerRef = React.useRef(null);
1082
- const videoRef = React.useRef(null);
1083
- const playerRef = React.useRef(null);
1084
- const seekRef = React.useRef(null);
1085
- const hideTimer = React.useRef(null);
1086
- const [playerState, setPlayerState] = React.useState(exports.PlayerState.IDLE);
1087
- const [currentTime, setCurrentTime] = React.useState(0);
1088
- const [duration, setDuration] = React.useState(0);
1089
- const [buffered, setBuffered] = React.useState(0);
1090
- const [volume, setVolume] = React.useState(1);
1091
- const [muted, setMuted] = React.useState(false);
1092
- const [showControls, setShowControls] = React.useState(true);
1093
- const [isFullscreen, setIsFullscreen] = React.useState(false);
1094
- const [showMenu, setShowMenu] = React.useState(null);
1095
- const [qualityLevels, setQualityLevels] = React.useState([]);
1096
- const [currentQualityIdx, setCurrentQualityIdx] = React.useState(-1);
1097
- const [playbackRate, setPlaybackRate] = React.useState(1);
1098
- const [seekTooltip, setSeekTooltip] = React.useState(null);
1099
- const [error, setError] = React.useState(null);
1100
- const [watermarkText, setWatermarkText] = React.useState(null);
1101
- React.useEffect(() => { injectPlayerStyles(accentColor); }, [accentColor]);
1102
- React.useEffect(() => {
1103
- if (!videoRef.current || playerRef.current)
1104
- return;
1105
- const player = new GuardVideoPlayer(videoRef.current, videoId, {
1106
- embedTokenEndpoint,
1107
- apiBaseUrl,
1108
- debug,
1109
- autoplay,
1357
+ class PlayerUI {
1358
+ constructor(container, videoId, config) {
1359
+ this.playerState = exports.PlayerState.IDLE;
1360
+ this.duration = 0;
1361
+ this.bufferedEnd = 0;
1362
+ this.qualityLevels = [];
1363
+ this.currentQualityIdx = -1;
1364
+ this.playbackRate = 1;
1365
+ this.openMenu = null;
1366
+ this.hideTimer = null;
1367
+ this.seekDragging = false;
1368
+ const accent = config.branding?.accentColor ?? '#00e5a0';
1369
+ const brandName = config.branding?.name ?? 'GuardVideo';
1370
+ injectStyles();
1371
+ container.innerHTML = '';
1372
+ this.root = el('div', 'gvp-root');
1373
+ this.root.style.width = config.width ?? '100%';
1374
+ this.root.style.height = config.height ?? 'auto';
1375
+ this.root.style.setProperty('--gvp-accent', accent);
1376
+ this.root.style.setProperty('--gvp-accent-dim', this._hexToRgba(accent, 0.18));
1377
+ this.root.style.setProperty('--gvp-accent-glow', this._hexToRgba(accent, 0.35));
1378
+ this.root.setAttribute('tabindex', '0');
1379
+ this.root.setAttribute('role', 'region');
1380
+ this.root.setAttribute('aria-label', `${brandName} video player`);
1381
+ this.videoEl = el('video', 'gvp-video', { playsinline: '', preload: 'metadata' });
1382
+ this.badge = el('div', 'gvp-badge');
1383
+ this.badge.setAttribute('aria-hidden', 'true');
1384
+ this.badge.appendChild(svgEl(ICON.shield, 13, 13));
1385
+ this.badge.appendChild(document.createTextNode(brandName));
1386
+ this.watermarkDiv = el('div', 'gvp-watermark');
1387
+ this.watermarkDiv.setAttribute('aria-hidden', 'true');
1388
+ this.spinner = el('div', 'gvp-spinner gvp-hidden');
1389
+ this.spinner.setAttribute('aria-label', 'Loading');
1390
+ this.spinner.setAttribute('role', 'status');
1391
+ this.spinner.appendChild(el('div', 'gvp-spinner-ring'));
1392
+ this.errorOverlay = el('div', 'gvp-error gvp-hidden');
1393
+ this.errorOverlay.setAttribute('role', 'alert');
1394
+ const errIcon = el('div', 'gvp-error-icon');
1395
+ errIcon.textContent = '⚠️';
1396
+ errIcon.setAttribute('aria-hidden', 'true');
1397
+ const errCode = el('div', 'gvp-error-code');
1398
+ const errMsg = el('div', 'gvp-error-msg');
1399
+ this.errorOverlay.append(errIcon, errCode, errMsg);
1400
+ this.centerPlay = el('div', 'gvp-center-play');
1401
+ this.centerPlay.setAttribute('role', 'button');
1402
+ this.centerPlay.setAttribute('aria-label', 'Play');
1403
+ this.centerPlayBtn = el('div', 'gvp-center-play-btn');
1404
+ this.centerPlayBtn.appendChild(svgEl(ICON.play, 30, 30));
1405
+ this.centerPlay.appendChild(this.centerPlayBtn);
1406
+ this.clickArea = el('div', 'gvp-click-area gvp-hidden');
1407
+ this.clickArea.setAttribute('aria-hidden', 'true');
1408
+ this.controls = el('div', 'gvp-controls');
1409
+ const inner = el('div', 'gvp-controls-inner');
1410
+ const seekRow = el('div', 'gvp-seek-row');
1411
+ this.seekWrap = el('div', 'gvp-seek-wrap');
1412
+ this.seekWrap.setAttribute('role', 'slider');
1413
+ this.seekWrap.setAttribute('aria-label', 'Seek');
1414
+ this.seekWrap.setAttribute('aria-valuemin', '0');
1415
+ this.seekWrap.setAttribute('aria-valuemax', '100');
1416
+ this.seekWrap.setAttribute('aria-valuenow', '0');
1417
+ this.seekWrap.setAttribute('aria-valuetext', '0:00 of 0:00');
1418
+ this.seekWrap.setAttribute('tabindex', '0');
1419
+ const seekTrack = el('div', 'gvp-seek-track');
1420
+ this.seekBuffered = el('div', 'gvp-seek-buffered');
1421
+ this.seekProgress = el('div', 'gvp-seek-progress');
1422
+ this.seekThumb = el('div', 'gvp-seek-thumb');
1423
+ this.seekTooltip = el('div', 'gvp-seek-tooltip');
1424
+ this.seekTooltip.setAttribute('aria-hidden', 'true');
1425
+ seekTrack.append(this.seekBuffered, this.seekProgress);
1426
+ this.seekWrap.append(seekTrack, this.seekThumb, this.seekTooltip);
1427
+ seekRow.appendChild(this.seekWrap);
1428
+ inner.appendChild(seekRow);
1429
+ const btnRow = el('div', 'gvp-btn-row');
1430
+ this.playBtn = el('button', 'gvp-btn gvp-btn-play');
1431
+ this.playBtn.type = 'button';
1432
+ this.playBtn.setAttribute('aria-label', 'Play');
1433
+ this.playBtn.title = 'Play (k)';
1434
+ this.playBtn.appendChild(svgEl(ICON.play));
1435
+ const volWrap = el('div', 'gvp-volume-wrap');
1436
+ this.volBtn = el('button', 'gvp-btn');
1437
+ this.volBtn.type = 'button';
1438
+ this.volBtn.setAttribute('aria-label', 'Mute');
1439
+ this.volBtn.title = 'Mute (m)';
1440
+ this.volBtn.appendChild(svgEl(ICON.volHigh, 18, 18));
1441
+ this.volSlider = el('input', 'gvp-volume-slider');
1442
+ Object.assign(this.volSlider, { type: 'range', min: '0', max: '1', step: '0.02', value: '1' });
1443
+ this.volSlider.setAttribute('aria-label', 'Volume');
1444
+ volWrap.append(this.volBtn, this.volSlider);
1445
+ this.timeEl = el('span', 'gvp-time');
1446
+ this.timeEl.setAttribute('aria-live', 'off');
1447
+ this.timeEl.textContent = '0:00 / 0:00';
1448
+ const spacer = el('div', 'gvp-spacer');
1449
+ const divider = el('div', 'gvp-divider');
1450
+ divider.setAttribute('aria-hidden', 'true');
1451
+ const speedWrap = el('div', 'gvp-menu-wrap');
1452
+ this.speedLabel = el('span', 'gvp-rate-label');
1453
+ this.speedLabel.textContent = '1×';
1454
+ this.speedLabel.setAttribute('role', 'button');
1455
+ this.speedLabel.setAttribute('aria-label', 'Playback speed');
1456
+ this.speedLabel.setAttribute('aria-haspopup', 'menu');
1457
+ this.speedLabel.setAttribute('tabindex', '0');
1458
+ this.speedMenu = el('div', 'gvp-menu gvp-hidden');
1459
+ this.speedMenu.setAttribute('role', 'menu');
1460
+ this.speedMenu.setAttribute('aria-label', 'Playback speed');
1461
+ const speedTitle = el('div', 'gvp-menu-title');
1462
+ speedTitle.setAttribute('aria-hidden', 'true');
1463
+ speedTitle.textContent = 'Speed';
1464
+ this.speedMenu.appendChild(speedTitle);
1465
+ SPEEDS.forEach(r => {
1466
+ const active = r === 1;
1467
+ const item = el('div', active ? 'gvp-menu-item gvp-menu-item-active' : 'gvp-menu-item');
1468
+ item.setAttribute('role', 'menuitemradio');
1469
+ item.setAttribute('aria-checked', active ? 'true' : 'false');
1470
+ item.dataset['speed'] = String(r);
1471
+ item.textContent = r === 1 ? 'Normal' : `${r}×`;
1472
+ if (active) {
1473
+ const check = el('span', 'gvp-menu-check');
1474
+ check.setAttribute('aria-hidden', 'true');
1475
+ check.textContent = '✓';
1476
+ item.appendChild(check);
1477
+ }
1478
+ this.speedMenu.appendChild(item);
1479
+ });
1480
+ speedWrap.append(this.speedLabel, this.speedMenu);
1481
+ const qualWrap = el('div', 'gvp-menu-wrap');
1482
+ this.settingsBtn = el('button', 'gvp-btn');
1483
+ this.settingsBtn.type = 'button';
1484
+ this.settingsBtn.setAttribute('aria-label', 'Quality settings');
1485
+ this.settingsBtn.setAttribute('aria-haspopup', 'menu');
1486
+ this.settingsBtn.title = 'Quality';
1487
+ this.settingsBtn.appendChild(svgEl(ICON.settings, 18, 18));
1488
+ this.settingsBtn.classList.add('gvp-hidden');
1489
+ this.qualityMenu = el('div', 'gvp-menu gvp-hidden');
1490
+ this.qualityMenu.setAttribute('role', 'menu');
1491
+ this.qualityMenu.setAttribute('aria-label', 'Video quality');
1492
+ qualWrap.append(this.settingsBtn, this.qualityMenu);
1493
+ this.fsBtn = el('button', 'gvp-btn');
1494
+ this.fsBtn.type = 'button';
1495
+ this.fsBtn.setAttribute('aria-label', 'Enter fullscreen');
1496
+ this.fsBtn.title = 'Fullscreen (f)';
1497
+ this.fsBtn.appendChild(svgEl(ICON.fsEnter, 18, 18));
1498
+ btnRow.append(this.playBtn, volWrap, this.timeEl, spacer, speedWrap, divider, qualWrap, this.fsBtn);
1499
+ inner.appendChild(btnRow);
1500
+ this.controls.appendChild(inner);
1501
+ this.root.append(this.videoEl, this.badge, this.watermarkDiv, this.spinner, this.errorOverlay, this.centerPlay, this.clickArea, this.controls);
1502
+ container.appendChild(this.root);
1503
+ this._onFsChangeBound = () => this._onFsChange();
1504
+ this._seekMouseMoveBound = (e) => { if (this.seekDragging)
1505
+ this._seekTo(e.clientX); };
1506
+ this._seekMouseUpBound = () => this._endSeekDrag();
1507
+ this._seekTouchMoveBound = (e) => {
1508
+ if (!this.seekDragging)
1509
+ return;
1510
+ e.preventDefault();
1511
+ this._seekTo(e.touches[0].clientX);
1512
+ };
1513
+ this._seekTouchEndBound = () => this._endSeekDrag();
1514
+ this._wireEvents(videoId, config);
1515
+ if (config.forensicWatermark !== false) {
1516
+ const wmText = config.viewerEmail || config.viewerName || '';
1517
+ if (wmText)
1518
+ this._renderWatermark(wmText);
1519
+ }
1520
+ }
1521
+ _hexToRgba(hex, alpha) {
1522
+ const clean = hex.replace('#', '');
1523
+ const full = clean.length === 3
1524
+ ? clean.split('').map(c => c + c).join('')
1525
+ : clean;
1526
+ const r = parseInt(full.substring(0, 2), 16);
1527
+ const g = parseInt(full.substring(2, 4), 16);
1528
+ const b = parseInt(full.substring(4, 6), 16);
1529
+ if (isNaN(r) || isNaN(g) || isNaN(b))
1530
+ return `rgba(0,229,160,${alpha})`;
1531
+ return `rgba(${r},${g},${b},${alpha})`;
1532
+ }
1533
+ _wireEvents(videoId, config) {
1534
+ const video = this.videoEl;
1535
+ this.corePlayer = new GuardVideoPlayer(video, videoId, {
1536
+ ...config,
1110
1537
  controls: false,
1111
- className: '',
1112
- style: {},
1113
- hlsConfig,
1114
- branding,
1115
- contextMenuItems,
1116
- security,
1117
- viewerName,
1118
- viewerEmail,
1119
- forensicWatermark,
1538
+ forensicWatermark: config.forensicWatermark !== false,
1120
1539
  onReady: () => {
1121
- onReady?.();
1122
- setTimeout(() => {
1123
- const levels = player.getQualityLevels();
1124
- setQualityLevels(levels);
1125
- }, 100);
1540
+ config.onReady?.();
1541
+ const levels = this.corePlayer.getQualityLevels();
1542
+ if (levels.length) {
1543
+ this.qualityLevels = levels;
1544
+ this._buildQualityMenu();
1545
+ }
1546
+ else {
1547
+ setTimeout(() => {
1548
+ this.qualityLevels = this.corePlayer.getQualityLevels();
1549
+ if (this.qualityLevels.length)
1550
+ this._buildQualityMenu();
1551
+ }, 100);
1552
+ }
1126
1553
  },
1127
1554
  onError: (err) => {
1128
- setError(err);
1129
- onError?.(err);
1555
+ this._showError(err);
1556
+ config.onError?.(err);
1130
1557
  },
1131
1558
  onQualityChange: (quality) => {
1132
- onQualityChange?.(quality);
1133
- const idx = player.getQualityLevels().findIndex(l => l.name === quality);
1134
- setCurrentQualityIdx(idx);
1559
+ const idx = this.qualityLevels.findIndex(l => l.name === quality);
1560
+ this.currentQualityIdx = idx;
1561
+ this._refreshQualityMenu();
1562
+ config.onQualityChange?.(quality);
1135
1563
  },
1136
1564
  onStateChange: (state) => {
1137
- setPlayerState(state);
1138
- onStateChange?.(state);
1565
+ this._onStateChange(state);
1566
+ config.onStateChange?.(state);
1139
1567
  },
1140
1568
  });
1141
- playerRef.current = player;
1142
- return () => {
1143
- player.destroy();
1144
- playerRef.current = null;
1145
- };
1146
- }, [videoId, embedTokenEndpoint]);
1147
- React.useEffect(() => {
1148
- const video = videoRef.current;
1149
- if (!video)
1150
- return;
1151
- const onTimeUpdate = () => {
1152
- setCurrentTime(video.currentTime);
1153
- onTimeUpdateProp?.(video.currentTime);
1154
- };
1155
- const onDuration = () => setDuration(video.duration || 0);
1156
- const onProgress = () => {
1569
+ video.addEventListener('timeupdate', () => {
1570
+ config.onTimeUpdate?.(video.currentTime);
1571
+ this._onTimeUpdate();
1572
+ });
1573
+ video.addEventListener('loadedmetadata', () => {
1574
+ this.duration = video.duration || 0;
1575
+ this._onTimeUpdate();
1576
+ });
1577
+ video.addEventListener('durationchange', () => {
1578
+ this.duration = video.duration || 0;
1579
+ });
1580
+ video.addEventListener('progress', () => {
1157
1581
  if (video.buffered.length > 0) {
1158
- setBuffered(video.buffered.end(video.buffered.length - 1));
1582
+ this.bufferedEnd = video.buffered.end(video.buffered.length - 1);
1583
+ this._updateSeekBar();
1584
+ }
1585
+ });
1586
+ video.addEventListener('volumechange', () => this._onVolumeChange());
1587
+ video.addEventListener('ended', () => {
1588
+ config.onEnded?.();
1589
+ this._showControls(true);
1590
+ this._renderPlayBtn();
1591
+ });
1592
+ video.addEventListener('ratechange', () => {
1593
+ this.playbackRate = video.playbackRate;
1594
+ const isNormal = this.playbackRate === 1;
1595
+ this.speedLabel.textContent = isNormal ? '1×' : `${this.playbackRate}×`;
1596
+ this.speedLabel.classList.toggle('gvp-rate-label-active', !isNormal);
1597
+ });
1598
+ this.playBtn.addEventListener('click', (e) => { e.stopPropagation(); this._togglePlay(); });
1599
+ this.volBtn.addEventListener('click', (e) => { e.stopPropagation(); this._toggleMute(); });
1600
+ this.volSlider.addEventListener('input', () => {
1601
+ video.volume = parseFloat(this.volSlider.value);
1602
+ video.muted = video.volume === 0;
1603
+ });
1604
+ this.seekWrap.addEventListener('mousedown', (e) => {
1605
+ e.preventDefault();
1606
+ this._startSeekDrag();
1607
+ this._seekTo(e.clientX);
1608
+ window.addEventListener('mousemove', this._seekMouseMoveBound);
1609
+ window.addEventListener('mouseup', this._seekMouseUpBound);
1610
+ });
1611
+ this.seekWrap.addEventListener('mousemove', (e) => this._onSeekHover(e.clientX));
1612
+ this.seekWrap.addEventListener('mouseleave', () => {
1613
+ this.seekTooltip.style.opacity = '0';
1614
+ });
1615
+ this.seekWrap.addEventListener('touchstart', (e) => {
1616
+ e.preventDefault();
1617
+ this._startSeekDrag();
1618
+ this._seekTo(e.touches[0].clientX);
1619
+ window.addEventListener('touchmove', this._seekTouchMoveBound, { passive: false });
1620
+ window.addEventListener('touchend', this._seekTouchEndBound);
1621
+ }, { passive: false });
1622
+ this.seekWrap.addEventListener('keydown', (e) => {
1623
+ if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight')
1624
+ return;
1625
+ e.preventDefault();
1626
+ const delta = e.key === 'ArrowLeft' ? -5 : 5;
1627
+ this.corePlayer.seek(Math.max(0, Math.min(this.duration, video.currentTime + delta)));
1628
+ this._resetHideTimer();
1629
+ });
1630
+ this.speedLabel.addEventListener('click', (e) => { e.stopPropagation(); this._toggleMenu('speed'); });
1631
+ this.speedLabel.addEventListener('keydown', (e) => {
1632
+ if (e.key === 'Enter' || e.key === ' ') {
1633
+ e.preventDefault();
1634
+ this._toggleMenu('speed');
1635
+ }
1636
+ });
1637
+ this.speedMenu.addEventListener('click', (e) => {
1638
+ e.stopPropagation();
1639
+ const item = e.target.closest('[data-speed]');
1640
+ if (!item)
1641
+ return;
1642
+ video.playbackRate = parseFloat(item.dataset['speed']);
1643
+ this._closeMenus();
1644
+ });
1645
+ this.settingsBtn.addEventListener('click', (e) => { e.stopPropagation(); this._toggleMenu('quality'); });
1646
+ this.qualityMenu.addEventListener('click', (e) => {
1647
+ e.stopPropagation();
1648
+ const item = e.target.closest('[data-quality]');
1649
+ if (!item)
1650
+ return;
1651
+ const idx = parseInt(item.dataset['quality']);
1652
+ this.corePlayer.setQuality(idx);
1653
+ this.currentQualityIdx = idx;
1654
+ this._refreshQualityMenu();
1655
+ this._closeMenus();
1656
+ });
1657
+ this.clickArea.addEventListener('click', (e) => { this._addRipple(e); this._togglePlay(); });
1658
+ this.centerPlay.addEventListener('click', (e) => { e.stopPropagation(); this._togglePlay(); });
1659
+ this.fsBtn.addEventListener('click', (e) => { e.stopPropagation(); this._toggleFullscreen(); });
1660
+ document.addEventListener('fullscreenchange', this._onFsChangeBound);
1661
+ document.addEventListener('webkitfullscreenchange', this._onFsChangeBound);
1662
+ document.addEventListener('mozfullscreenchange', this._onFsChangeBound);
1663
+ document.addEventListener('MSFullscreenChange', this._onFsChangeBound);
1664
+ this.root.addEventListener('mousemove', () => this._resetHideTimer());
1665
+ this.root.addEventListener('touchstart', () => this._resetHideTimer(), { passive: true });
1666
+ this.root.addEventListener('mouseleave', () => {
1667
+ if (this.playerState === exports.PlayerState.PLAYING)
1668
+ this._showControls(false);
1669
+ });
1670
+ this.root.addEventListener('click', () => this._closeMenus());
1671
+ this.root.addEventListener('keydown', (e) => this._onKeyDown(e));
1672
+ }
1673
+ _onStateChange(state) {
1674
+ this.playerState = state;
1675
+ const loading = state === exports.PlayerState.LOADING || state === exports.PlayerState.BUFFERING;
1676
+ const idle = state === exports.PlayerState.IDLE;
1677
+ const ended = this.videoEl.ended;
1678
+ this.spinner.classList.toggle('gvp-hidden', !loading);
1679
+ const showCenter = (idle || ended) && !loading;
1680
+ this.centerPlay.classList.toggle('gvp-hidden', !showCenter);
1681
+ if (showCenter) {
1682
+ this.centerPlayBtn.innerHTML = '';
1683
+ this.centerPlayBtn.appendChild(svgEl(ended ? ICON.replay : ICON.play, 30, 30));
1684
+ this.centerPlay.setAttribute('aria-label', ended ? 'Replay' : 'Play');
1685
+ }
1686
+ this.clickArea.classList.toggle('gvp-hidden', idle);
1687
+ if (state !== exports.PlayerState.PLAYING) {
1688
+ this._showControls(true);
1689
+ if (this.hideTimer) {
1690
+ clearTimeout(this.hideTimer);
1691
+ this.hideTimer = null;
1159
1692
  }
1160
- };
1161
- const onVolumeChange = () => {
1162
- setVolume(video.volume);
1163
- setMuted(video.muted);
1164
- };
1165
- const onEnded_ = () => {
1166
- onEnded?.();
1167
- setShowControls(true);
1168
- };
1169
- const onRateChange = () => setPlaybackRate(video.playbackRate);
1170
- video.addEventListener('timeupdate', onTimeUpdate);
1171
- video.addEventListener('loadedmetadata', onDuration);
1172
- video.addEventListener('durationchange', onDuration);
1173
- video.addEventListener('progress', onProgress);
1174
- video.addEventListener('volumechange', onVolumeChange);
1175
- video.addEventListener('ended', onEnded_);
1176
- video.addEventListener('ratechange', onRateChange);
1177
- return () => {
1178
- video.removeEventListener('timeupdate', onTimeUpdate);
1179
- video.removeEventListener('loadedmetadata', onDuration);
1180
- video.removeEventListener('durationchange', onDuration);
1181
- video.removeEventListener('progress', onProgress);
1182
- video.removeEventListener('volumechange', onVolumeChange);
1183
- video.removeEventListener('ended', onEnded_);
1184
- video.removeEventListener('ratechange', onRateChange);
1185
- };
1186
- }, [onEnded, onTimeUpdateProp]);
1187
- React.useEffect(() => {
1188
- if (!forensicWatermark)
1189
- return;
1190
- const t = setTimeout(() => {
1191
- const email = viewerEmail || viewerName || '';
1192
- if (email)
1193
- setWatermarkText(email);
1194
- }, 1500);
1195
- return () => clearTimeout(t);
1196
- }, [forensicWatermark, viewerEmail, viewerName]);
1197
- const resetHideTimer = React.useCallback(() => {
1198
- setShowControls(true);
1199
- if (hideTimer.current)
1200
- clearTimeout(hideTimer.current);
1201
- hideTimer.current = setTimeout(() => {
1202
- if (playerState === exports.PlayerState.PLAYING)
1203
- setShowControls(false);
1204
- }, 2800);
1205
- }, [playerState]);
1206
- React.useEffect(() => {
1207
- if (playerState !== exports.PlayerState.PLAYING) {
1208
- setShowControls(true);
1209
- if (hideTimer.current)
1210
- clearTimeout(hideTimer.current);
1211
1693
  }
1212
1694
  else {
1213
- resetHideTimer();
1695
+ this._resetHideTimer();
1214
1696
  }
1215
- return () => { if (hideTimer.current)
1216
- clearTimeout(hideTimer.current); };
1217
- }, [playerState, resetHideTimer]);
1218
- React.useEffect(() => {
1219
- const onFsChange = () => setIsFullscreen(!!document.fullscreenElement);
1220
- document.addEventListener('fullscreenchange', onFsChange);
1221
- return () => document.removeEventListener('fullscreenchange', onFsChange);
1222
- }, []);
1223
- React.useEffect(() => {
1224
- const onKey = (e) => {
1225
- if (!containerRef.current?.contains(document.activeElement) &&
1226
- document.activeElement !== containerRef.current)
1227
- return;
1228
- switch (e.code) {
1229
- case 'Space':
1230
- case 'KeyK':
1231
- e.preventDefault();
1232
- togglePlay();
1233
- break;
1234
- case 'ArrowLeft':
1235
- e.preventDefault();
1236
- playerRef.current?.seek(Math.max(0, (videoRef.current?.currentTime || 0) - 5));
1237
- break;
1238
- case 'ArrowRight':
1239
- e.preventDefault();
1240
- playerRef.current?.seek(Math.min(duration, (videoRef.current?.currentTime || 0) + 5));
1241
- break;
1242
- case 'ArrowUp':
1243
- e.preventDefault();
1244
- setVolume_(Math.min(1, volume + 0.1));
1245
- break;
1246
- case 'ArrowDown':
1247
- e.preventDefault();
1248
- setVolume_(Math.max(0, volume - 0.1));
1249
- break;
1250
- case 'KeyM':
1251
- e.preventDefault();
1252
- toggleMute();
1253
- break;
1254
- case 'KeyF':
1255
- e.preventDefault();
1256
- toggleFullscreen();
1257
- break;
1258
- }
1259
- };
1260
- document.addEventListener('keydown', onKey);
1261
- return () => document.removeEventListener('keydown', onKey);
1262
- }, [duration, volume]);
1263
- React.useImperativeHandle(ref, () => ({
1264
- play: () => playerRef.current?.play() || Promise.resolve(),
1265
- pause: () => playerRef.current?.pause(),
1266
- getCurrentTime: () => playerRef.current?.getCurrentTime() || 0,
1267
- seek: (t) => playerRef.current?.seek(t),
1268
- getDuration: () => playerRef.current?.getDuration() || 0,
1269
- getVolume: () => playerRef.current?.getVolume() || 1,
1270
- setVolume: (v) => playerRef.current?.setVolume(v),
1271
- getQualityLevels: () => playerRef.current?.getQualityLevels() || [],
1272
- getCurrentQuality: () => playerRef.current?.getCurrentQuality() || null,
1273
- setQuality: (i) => playerRef.current?.setQuality(i),
1274
- getState: () => playerRef.current?.getState() || exports.PlayerState.IDLE,
1275
- destroy: () => playerRef.current?.destroy(),
1276
- getVideoElement: () => videoRef.current,
1277
- }));
1278
- const togglePlay = React.useCallback(() => {
1279
- const video = videoRef.current;
1280
- if (!video)
1281
- return;
1282
- if (video.paused || video.ended) {
1283
- playerRef.current?.play().catch(() => { });
1697
+ this._renderPlayBtn();
1698
+ }
1699
+ _togglePlay() {
1700
+ const v = this.videoEl;
1701
+ if (v.paused || v.ended) {
1702
+ this.corePlayer.play().catch(() => { });
1284
1703
  }
1285
1704
  else {
1286
- playerRef.current?.pause();
1705
+ this.corePlayer.pause();
1287
1706
  }
1288
- resetHideTimer();
1289
- }, [resetHideTimer]);
1290
- const setVolume_ = React.useCallback((v) => {
1291
- if (!videoRef.current)
1707
+ this._resetHideTimer();
1708
+ }
1709
+ _renderPlayBtn() {
1710
+ const ended = this.videoEl.ended;
1711
+ const playing = this.playerState === exports.PlayerState.PLAYING;
1712
+ const icon = ended ? ICON.replay : playing ? ICON.pause : ICON.play;
1713
+ const label = playing ? 'Pause' : 'Play';
1714
+ this.playBtn.innerHTML = '';
1715
+ this.playBtn.appendChild(svgEl(icon));
1716
+ this.playBtn.setAttribute('aria-label', label);
1717
+ this.playBtn.title = `${label} (k)`;
1718
+ }
1719
+ _toggleMute() { this.videoEl.muted = !this.videoEl.muted; }
1720
+ _onVolumeChange() {
1721
+ const v = this.videoEl;
1722
+ const vol = v.muted ? 0 : v.volume;
1723
+ const muted = vol === 0;
1724
+ const icon = muted ? ICON.volMute : vol < 0.4 ? ICON.volLow : vol < 0.75 ? ICON.volMid : ICON.volHigh;
1725
+ this.volBtn.innerHTML = '';
1726
+ this.volBtn.appendChild(svgEl(icon, 18, 18));
1727
+ this.volBtn.setAttribute('aria-label', muted ? 'Unmute' : 'Mute');
1728
+ this.volBtn.title = muted ? 'Unmute (m)' : 'Mute (m)';
1729
+ this.volSlider.value = String(vol);
1730
+ }
1731
+ _startSeekDrag() {
1732
+ this.seekDragging = true;
1733
+ this.seekWrap.classList.add('gvp-dragging');
1734
+ }
1735
+ _endSeekDrag() {
1736
+ this.seekDragging = false;
1737
+ this.seekWrap.classList.remove('gvp-dragging');
1738
+ window.removeEventListener('mousemove', this._seekMouseMoveBound);
1739
+ window.removeEventListener('mouseup', this._seekMouseUpBound);
1740
+ window.removeEventListener('touchmove', this._seekTouchMoveBound);
1741
+ window.removeEventListener('touchend', this._seekTouchEndBound);
1742
+ this._resetHideTimer();
1743
+ }
1744
+ _seekTo(clientX) {
1745
+ if (!this.duration)
1292
1746
  return;
1293
- videoRef.current.volume = v;
1294
- videoRef.current.muted = v === 0;
1295
- }, []);
1296
- const toggleMute = React.useCallback(() => {
1297
- if (!videoRef.current)
1747
+ const rect = this.seekWrap.getBoundingClientRect();
1748
+ const pct = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
1749
+ this.corePlayer.seek(pct * this.duration);
1750
+ }
1751
+ _onSeekHover(clientX) {
1752
+ if (!this.duration)
1298
1753
  return;
1299
- videoRef.current.muted = !videoRef.current.muted;
1300
- }, []);
1301
- const toggleFullscreen = React.useCallback(() => {
1302
- const el = containerRef.current;
1303
- if (!el)
1754
+ const rect = this.seekWrap.getBoundingClientRect();
1755
+ const pct = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
1756
+ this.seekTooltip.textContent = formatTime(pct * this.duration);
1757
+ this.seekTooltip.style.left = `${pct * 100}%`;
1758
+ this.seekTooltip.style.opacity = '1';
1759
+ }
1760
+ _onTimeUpdate() {
1761
+ const ct = this.videoEl.currentTime;
1762
+ const dur = this.duration;
1763
+ this.timeEl.textContent = `${formatTime(ct)} / ${formatTime(dur)}`;
1764
+ this._updateSeekBar();
1765
+ const pct = dur > 0 ? Math.round((ct / dur) * 100) : 0;
1766
+ this.seekWrap.setAttribute('aria-valuenow', String(pct));
1767
+ this.seekWrap.setAttribute('aria-valuetext', `${formatTime(ct)} of ${formatTime(dur)}`);
1768
+ }
1769
+ _updateSeekBar() {
1770
+ const ct = this.videoEl.currentTime;
1771
+ const dur = this.duration;
1772
+ if (!dur)
1304
1773
  return;
1305
- if (document.fullscreenElement) {
1306
- document.exitFullscreen().catch(() => { });
1774
+ const pPct = (ct / dur) * 100;
1775
+ const bPct = (this.bufferedEnd / dur) * 100;
1776
+ this.seekProgress.style.width = `${pPct}%`;
1777
+ this.seekBuffered.style.width = `${bPct}%`;
1778
+ this.seekThumb.style.left = `${pPct}%`;
1779
+ }
1780
+ _buildQualityMenu() {
1781
+ if (!this.qualityLevels.length)
1782
+ return;
1783
+ this.settingsBtn.classList.remove('gvp-hidden');
1784
+ this.qualityMenu.innerHTML = '';
1785
+ const title = el('div', 'gvp-menu-title');
1786
+ title.setAttribute('aria-hidden', 'true');
1787
+ title.textContent = 'Quality';
1788
+ const autoItem = el('div', 'gvp-menu-item gvp-menu-item-active');
1789
+ autoItem.setAttribute('role', 'menuitemradio');
1790
+ autoItem.setAttribute('aria-checked', 'true');
1791
+ autoItem.dataset['quality'] = '-1';
1792
+ autoItem.textContent = 'Auto';
1793
+ const checkEl = el('span', 'gvp-menu-check');
1794
+ checkEl.setAttribute('aria-hidden', 'true');
1795
+ checkEl.textContent = '✓';
1796
+ autoItem.appendChild(checkEl);
1797
+ this.qualityMenu.append(title, autoItem, el('div', 'gvp-menu-sep'));
1798
+ [...this.qualityLevels].reverse().forEach(q => {
1799
+ const item = el('div', 'gvp-menu-item');
1800
+ item.setAttribute('role', 'menuitemradio');
1801
+ item.setAttribute('aria-checked', 'false');
1802
+ item.dataset['quality'] = String(q.index);
1803
+ item.textContent = q.name;
1804
+ this.qualityMenu.appendChild(item);
1805
+ });
1806
+ this.currentQualityIdx = -1;
1807
+ }
1808
+ _refreshQualityMenu() {
1809
+ this.qualityMenu.querySelectorAll('[data-quality]').forEach(item => {
1810
+ const active = parseInt(item.dataset['quality']) === this.currentQualityIdx;
1811
+ item.className = active ? 'gvp-menu-item gvp-menu-item-active' : 'gvp-menu-item';
1812
+ item.setAttribute('aria-checked', String(active));
1813
+ const existing = item.querySelector('.gvp-menu-check');
1814
+ if (active && !existing) {
1815
+ const c = el('span', 'gvp-menu-check');
1816
+ c.setAttribute('aria-hidden', 'true');
1817
+ c.textContent = '✓';
1818
+ item.appendChild(c);
1819
+ }
1820
+ else if (!active && existing) {
1821
+ existing.remove();
1822
+ }
1823
+ });
1824
+ }
1825
+ _refreshSpeedMenu() {
1826
+ this.speedMenu.querySelectorAll('[data-speed]').forEach(item => {
1827
+ const active = parseFloat(item.dataset['speed']) === this.playbackRate;
1828
+ item.className = active ? 'gvp-menu-item gvp-menu-item-active' : 'gvp-menu-item';
1829
+ item.setAttribute('aria-checked', String(active));
1830
+ const existing = item.querySelector('.gvp-menu-check');
1831
+ if (active && !existing) {
1832
+ const c = el('span', 'gvp-menu-check');
1833
+ c.setAttribute('aria-hidden', 'true');
1834
+ c.textContent = '✓';
1835
+ item.appendChild(c);
1836
+ }
1837
+ else if (!active && existing) {
1838
+ existing.remove();
1839
+ }
1840
+ });
1841
+ }
1842
+ _toggleMenu(which) {
1843
+ const same = this.openMenu === which;
1844
+ this._closeMenus();
1845
+ if (!same) {
1846
+ this.openMenu = which;
1847
+ if (which === 'speed') {
1848
+ this._refreshSpeedMenu();
1849
+ this.speedMenu.classList.remove('gvp-hidden');
1850
+ }
1851
+ else {
1852
+ this._refreshQualityMenu();
1853
+ this.qualityMenu.classList.remove('gvp-hidden');
1854
+ }
1855
+ }
1856
+ }
1857
+ _closeMenus() {
1858
+ this.openMenu = null;
1859
+ this.speedMenu.classList.add('gvp-hidden');
1860
+ this.qualityMenu.classList.add('gvp-hidden');
1861
+ }
1862
+ _showControls(visible) {
1863
+ this.controls.classList.toggle('gvp-controls-hidden', !visible);
1864
+ this.badge.classList.toggle('gvp-badge-hidden', !visible);
1865
+ }
1866
+ _resetHideTimer() {
1867
+ this._showControls(true);
1868
+ if (this.hideTimer)
1869
+ clearTimeout(this.hideTimer);
1870
+ this.hideTimer = setTimeout(() => {
1871
+ if (this.playerState === exports.PlayerState.PLAYING && !this.openMenu) {
1872
+ this._showControls(false);
1873
+ }
1874
+ }, 2800);
1875
+ }
1876
+ _toggleFullscreen() {
1877
+ const doc = document;
1878
+ const root = this.root;
1879
+ const isFs = !!(document.fullscreenElement ||
1880
+ doc.webkitFullscreenElement ||
1881
+ doc.mozFullScreenElement ||
1882
+ doc.msFullscreenElement);
1883
+ if (isFs) {
1884
+ (document.exitFullscreen?.() ||
1885
+ doc.webkitExitFullscreen?.() ||
1886
+ doc.mozCancelFullScreen?.() ||
1887
+ (doc.msExitFullscreen?.(), Promise.resolve()))
1888
+ ?.catch?.(() => { });
1307
1889
  }
1308
1890
  else {
1309
- el.requestFullscreen().catch(() => { });
1891
+ (root.requestFullscreen?.() ||
1892
+ root.webkitRequestFullscreen?.() ||
1893
+ root.mozRequestFullScreen?.() ||
1894
+ (root.msRequestFullscreen?.(), Promise.resolve()))
1895
+ ?.catch?.(() => { });
1310
1896
  }
1311
- }, []);
1312
- const getSeekedTime = (e) => {
1313
- const rect = seekRef.current.getBoundingClientRect();
1314
- const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
1315
- const pct = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
1316
- return pct * duration;
1317
- };
1318
- const onSeekClick = (e) => {
1319
- const t = getSeekedTime(e);
1320
- playerRef.current?.seek(t);
1321
- resetHideTimer();
1322
- };
1323
- const onSeekMouseMove = (e) => {
1324
- const rect = seekRef.current.getBoundingClientRect();
1325
- const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
1326
- setSeekTooltip({ pct, time: formatTime(pct * duration) });
1327
- };
1328
- const onSeekMouseLeave = () => setSeekTooltip(null);
1329
- const addRipple = (e) => {
1330
- const el = containerRef.current;
1331
- if (!el)
1332
- return;
1333
- const rect = el.getBoundingClientRect();
1897
+ }
1898
+ _onFsChange() {
1899
+ const doc = document;
1900
+ const fs = !!(document.fullscreenElement ||
1901
+ doc.webkitFullscreenElement ||
1902
+ doc.mozFullScreenElement ||
1903
+ doc.msFullscreenElement);
1904
+ this.fsBtn.innerHTML = '';
1905
+ this.fsBtn.appendChild(svgEl(fs ? ICON.fsExit : ICON.fsEnter, 18, 18));
1906
+ this.fsBtn.setAttribute('aria-label', fs ? 'Exit fullscreen' : 'Enter fullscreen');
1907
+ this.fsBtn.title = fs ? 'Exit fullscreen (f)' : 'Enter fullscreen (f)';
1908
+ }
1909
+ _showError(err) {
1910
+ this.errorOverlay.children[1].textContent = err.code;
1911
+ this.errorOverlay.children[2].textContent = err.message;
1912
+ this.errorOverlay.classList.remove('gvp-hidden');
1913
+ this.controls.classList.add('gvp-hidden');
1914
+ }
1915
+ _renderWatermark(text) {
1916
+ this.watermarkDiv.innerHTML = '';
1917
+ for (let i = 0; i < 20; i++) {
1918
+ const span = el('span', 'gvp-watermark-text');
1919
+ span.textContent = text;
1920
+ span.style.left = `${(i % 4) * 26 + (Math.floor(i / 4) % 2) * 13}%`;
1921
+ span.style.top = `${Math.floor(i / 4) * 22}%`;
1922
+ this.watermarkDiv.appendChild(span);
1923
+ }
1924
+ }
1925
+ _addRipple(e) {
1926
+ const rect = this.root.getBoundingClientRect();
1334
1927
  const d = document.createElement('div');
1335
1928
  d.className = 'gvp-ripple';
1336
- const size = 60;
1337
- d.style.cssText = `width:${size}px;height:${size}px;left:${e.clientX - rect.left - size / 2}px;top:${e.clientY - rect.top - size / 2}px`;
1338
- el.appendChild(d);
1339
- setTimeout(() => d.remove(), 600);
1340
- };
1341
- const progressPct = duration > 0 ? (currentTime / duration) * 100 : 0;
1342
- const bufferedPct = duration > 0 ? (buffered / duration) * 100 : 0;
1343
- const isLoading = playerState === exports.PlayerState.LOADING || playerState === exports.PlayerState.BUFFERING;
1344
- const isIdle = playerState === exports.PlayerState.IDLE;
1345
- const isPlaying = playerState === exports.PlayerState.PLAYING;
1346
- const isEnded = videoRef.current?.ended ?? false;
1347
- const controlsVisible = showControls || !isPlaying;
1348
- const SPEEDS = [0.5, 0.75, 1, 1.25, 1.5, 2];
1349
- return (React.createElement("div", { ref: containerRef, className: `gvp-root${className ? ` ${className}` : ''}`, style: { width, height, ...style }, tabIndex: 0, onMouseMove: resetHideTimer, onMouseLeave: () => isPlaying && setShowControls(false), onClick: () => setShowMenu(null) },
1350
- React.createElement("video", { ref: videoRef, className: "gvp-video", playsInline: true, preload: "metadata" }),
1351
- React.createElement("div", { className: `gvp-badge${controlsVisible ? '' : ' gvp-badge-hidden'}` },
1352
- React.createElement(ShieldIcon, { color: accentColor }),
1353
- brandName),
1354
- forensicWatermark && watermarkText && (React.createElement("div", { className: "gvp-watermark" }, Array.from({ length: 20 }).map((_, i) => (React.createElement("span", { key: i, className: "gvp-watermark-text", style: {
1355
- left: `${(i % 4) * 26 + ((Math.floor(i / 4)) % 2) * 13}%`,
1356
- top: `${Math.floor(i / 4) * 22}%`,
1357
- } }, watermarkText))))),
1358
- isLoading && (React.createElement("div", { className: "gvp-spinner" },
1359
- React.createElement("div", { className: "gvp-spinner-ring" }))),
1360
- error && (React.createElement("div", { className: "gvp-error" },
1361
- React.createElement("div", { className: "gvp-error-icon" }, "\u26A0\uFE0F"),
1362
- React.createElement("div", { className: "gvp-error-code" }, error.code),
1363
- React.createElement("div", { className: "gvp-error-msg" }, error.message))),
1364
- (isIdle || isEnded) && !error && !isLoading && (React.createElement("div", { className: "gvp-center-play", onClick: togglePlay },
1365
- React.createElement("div", { className: "gvp-center-play-btn" }, isEnded ? React.createElement(ReplayIcon, null) : React.createElement(PlayIcon, null)))),
1366
- !isIdle && !error && (React.createElement("div", { className: "gvp-click-area", onClick: (e) => { addRipple(e); togglePlay(); } })),
1367
- !error && (React.createElement("div", { className: `gvp-controls${controlsVisible ? '' : ' gvp-controls-hidden'}` },
1368
- React.createElement("div", { className: "gvp-seek-row" },
1369
- React.createElement("div", { ref: seekRef, className: "gvp-seek-wrap", onClick: onSeekClick, onMouseMove: onSeekMouseMove, onMouseLeave: onSeekMouseLeave },
1370
- React.createElement("div", { className: "gvp-seek-track" },
1371
- React.createElement("div", { className: "gvp-seek-buffered", style: { width: `${bufferedPct}%` } }),
1372
- React.createElement("div", { className: "gvp-seek-progress", style: { width: `${progressPct}%` } })),
1373
- React.createElement("div", { className: "gvp-seek-thumb", style: { left: `${progressPct}%` } }),
1374
- seekTooltip && (React.createElement("div", { className: "gvp-seek-tooltip", style: { left: `${seekTooltip.pct * 100}%` } }, seekTooltip.time)))),
1375
- React.createElement("div", { className: "gvp-btn-row" },
1376
- React.createElement("button", { className: "gvp-btn", onClick: togglePlay, title: isPlaying ? 'Pause (k)' : 'Play (k)' }, isEnded ? React.createElement(ReplayIcon, null) : isPlaying ? React.createElement(PauseIcon, null) : React.createElement(PlayIcon, null)),
1377
- React.createElement("div", { className: "gvp-volume-wrap" },
1378
- React.createElement("button", { className: "gvp-btn", onClick: toggleMute, title: "Mute (m)" }, muted || volume === 0 ? React.createElement(VolumeMuteIcon, null) : React.createElement(VolumeHighIcon, null)),
1379
- React.createElement("input", { type: "range", className: "gvp-volume-slider", min: 0, max: 1, step: 0.02, value: muted ? 0 : volume, onChange: (e) => setVolume_(parseFloat(e.target.value)), title: "Volume" })),
1380
- React.createElement("span", { className: "gvp-time" },
1381
- formatTime(currentTime),
1382
- "\u00A0",
1383
- React.createElement("span", { style: { opacity: 0.45 } }, "/"),
1384
- "\u00A0",
1385
- formatTime(duration)),
1386
- React.createElement("div", { className: "gvp-spacer" }),
1387
- React.createElement("div", { className: "gvp-menu-wrap" },
1388
- React.createElement("span", { className: "gvp-rate-label", title: "Playback speed", onClick: (e) => { e.stopPropagation(); setShowMenu(showMenu === 'speed' ? null : 'speed'); } }, playbackRate === 1 ? '1×' : `${playbackRate}×`),
1389
- showMenu === 'speed' && (React.createElement("div", { className: "gvp-menu", onClick: (e) => e.stopPropagation() },
1390
- React.createElement("div", { className: "gvp-menu-title" }, "Speed"),
1391
- SPEEDS.map((r) => (React.createElement("div", { key: r, className: `gvp-menu-item${playbackRate === r ? ' gvp-menu-item-active' : ''}`, onClick: () => {
1392
- if (videoRef.current)
1393
- videoRef.current.playbackRate = r;
1394
- setShowMenu(null);
1395
- } },
1396
- r === 1 ? 'Normal' : `${r}×`,
1397
- playbackRate === r && React.createElement("span", { className: "gvp-menu-check" }, "\u2713"))))))),
1398
- qualityLevels.length > 0 && (React.createElement("div", { className: "gvp-menu-wrap" },
1399
- React.createElement("button", { className: "gvp-btn", title: "Quality settings", onClick: (e) => { e.stopPropagation(); setShowMenu(showMenu === 'quality' ? null : 'quality'); } },
1400
- React.createElement(SettingsIcon, null)),
1401
- showMenu === 'quality' && (React.createElement("div", { className: "gvp-menu", onClick: (e) => e.stopPropagation() },
1402
- React.createElement("div", { className: "gvp-menu-title" }, "Quality"),
1403
- React.createElement("div", { className: `gvp-menu-item${currentQualityIdx === -1 ? ' gvp-menu-item-active' : ''}`, onClick: () => {
1404
- playerRef.current?.setQuality(-1);
1405
- setCurrentQualityIdx(-1);
1406
- setShowMenu(null);
1407
- } },
1408
- "Auto",
1409
- currentQualityIdx === -1 && React.createElement("span", { className: "gvp-menu-check" }, "\u2713")),
1410
- React.createElement("div", { className: "gvp-menu-sep" }),
1411
- [...qualityLevels].reverse().map((q) => (React.createElement("div", { key: q.index, className: `gvp-menu-item${currentQualityIdx === q.index ? ' gvp-menu-item-active' : ''}`, onClick: () => {
1412
- playerRef.current?.setQuality(q.index);
1413
- setCurrentQualityIdx(q.index);
1414
- setShowMenu(null);
1415
- } },
1416
- q.name,
1417
- currentQualityIdx === q.index && React.createElement("span", { className: "gvp-menu-check" }, "\u2713")))))))),
1418
- React.createElement("button", { className: "gvp-btn", onClick: toggleFullscreen, title: "Fullscreen (f)" }, isFullscreen ? React.createElement(ExitFullscreenIcon, null) : React.createElement(FullscreenIcon, null)))))));
1929
+ const sz = 60;
1930
+ d.style.cssText = `width:${sz}px;height:${sz}px;left:${e.clientX - rect.left - sz / 2}px;top:${e.clientY - rect.top - sz / 2}px`;
1931
+ this.root.appendChild(d);
1932
+ setTimeout(() => d.remove(), 650);
1933
+ }
1934
+ _onKeyDown(e) {
1935
+ switch (e.code) {
1936
+ case 'Space':
1937
+ case 'KeyK':
1938
+ e.preventDefault();
1939
+ this._togglePlay();
1940
+ break;
1941
+ case 'ArrowLeft':
1942
+ if (document.activeElement !== this.seekWrap) {
1943
+ e.preventDefault();
1944
+ this.corePlayer.seek(Math.max(0, this.videoEl.currentTime - 5));
1945
+ }
1946
+ break;
1947
+ case 'ArrowRight':
1948
+ if (document.activeElement !== this.seekWrap) {
1949
+ e.preventDefault();
1950
+ this.corePlayer.seek(Math.min(this.duration, this.videoEl.currentTime + 5));
1951
+ }
1952
+ break;
1953
+ case 'ArrowUp':
1954
+ e.preventDefault();
1955
+ this.videoEl.volume = Math.min(1, this.videoEl.volume + 0.1);
1956
+ break;
1957
+ case 'ArrowDown':
1958
+ e.preventDefault();
1959
+ this.videoEl.volume = Math.max(0, this.videoEl.volume - 0.1);
1960
+ break;
1961
+ case 'KeyM':
1962
+ e.preventDefault();
1963
+ this._toggleMute();
1964
+ break;
1965
+ case 'KeyF':
1966
+ e.preventDefault();
1967
+ this._toggleFullscreen();
1968
+ break;
1969
+ }
1970
+ }
1971
+ play() { return this.corePlayer.play(); }
1972
+ pause() { return this.corePlayer.pause(); }
1973
+ seek(t) { return this.corePlayer.seek(t); }
1974
+ getCurrentTime() { return this.corePlayer.getCurrentTime(); }
1975
+ getDuration() { return this.corePlayer.getDuration(); }
1976
+ getVolume() { return this.corePlayer.getVolume(); }
1977
+ setVolume(v) { return this.corePlayer.setVolume(v); }
1978
+ getQualityLevels() { return this.corePlayer.getQualityLevels(); }
1979
+ getCurrentQuality() { return this.corePlayer.getCurrentQuality(); }
1980
+ setQuality(i) { return this.corePlayer.setQuality(i); }
1981
+ getState() { return this.corePlayer.getState(); }
1982
+ getVideoElement() { return this.videoEl; }
1983
+ destroy() {
1984
+ if (this.hideTimer)
1985
+ clearTimeout(this.hideTimer);
1986
+ document.removeEventListener('fullscreenchange', this._onFsChangeBound);
1987
+ document.removeEventListener('webkitfullscreenchange', this._onFsChangeBound);
1988
+ document.removeEventListener('mozfullscreenchange', this._onFsChangeBound);
1989
+ document.removeEventListener('MSFullscreenChange', this._onFsChangeBound);
1990
+ window.removeEventListener('mousemove', this._seekMouseMoveBound);
1991
+ window.removeEventListener('mouseup', this._seekMouseUpBound);
1992
+ window.removeEventListener('touchmove', this._seekTouchMoveBound);
1993
+ window.removeEventListener('touchend', this._seekTouchEndBound);
1994
+ this.corePlayer.destroy();
1995
+ this.root.remove();
1996
+ }
1997
+ }
1998
+
1999
+ const GuardVideoPlayerComponent = React.forwardRef((props, ref) => {
2000
+ const { videoId, width = '100%', height = 'auto', className = '', style = {}, onReady, onError, onQualityChange, onStateChange, onTimeUpdate, onEnded, ...playerConfig } = props;
2001
+ const mountRef = React.useRef(null);
2002
+ const uiRef = React.useRef(null);
2003
+ const cbRef = React.useRef({ onReady, onError, onQualityChange, onStateChange, onTimeUpdate, onEnded });
2004
+ cbRef.current = { onReady, onError, onQualityChange, onStateChange, onTimeUpdate, onEnded };
2005
+ React.useEffect(() => {
2006
+ if (!mountRef.current)
2007
+ return;
2008
+ const config = {
2009
+ ...playerConfig,
2010
+ width: String(width),
2011
+ height: String(height),
2012
+ onReady: () => cbRef.current.onReady?.(),
2013
+ onError: (e) => cbRef.current.onError?.(e),
2014
+ onQualityChange: (q) => cbRef.current.onQualityChange?.(q),
2015
+ onStateChange: (s) => cbRef.current.onStateChange?.(s),
2016
+ onTimeUpdate: (t) => cbRef.current.onTimeUpdate?.(t),
2017
+ onEnded: () => cbRef.current.onEnded?.(),
2018
+ };
2019
+ const ui = new PlayerUI(mountRef.current, videoId, config);
2020
+ uiRef.current = ui;
2021
+ return () => { ui.destroy(); uiRef.current = null; };
2022
+ }, [videoId, playerConfig.embedTokenEndpoint]);
2023
+ React.useImperativeHandle(ref, () => ({
2024
+ play: () => uiRef.current?.play() ?? Promise.resolve(),
2025
+ pause: () => uiRef.current?.pause(),
2026
+ seek: (t) => uiRef.current?.seek(t),
2027
+ getCurrentTime: () => uiRef.current?.getCurrentTime() ?? 0,
2028
+ getDuration: () => uiRef.current?.getDuration() ?? 0,
2029
+ getVolume: () => uiRef.current?.getVolume() ?? 1,
2030
+ setVolume: (v) => uiRef.current?.setVolume(v),
2031
+ getQualityLevels: () => uiRef.current?.getQualityLevels() ?? [],
2032
+ getCurrentQuality: () => uiRef.current?.getCurrentQuality() ?? null,
2033
+ setQuality: (i) => uiRef.current?.setQuality(i),
2034
+ getState: () => uiRef.current?.getState() ?? exports.PlayerState.IDLE,
2035
+ destroy: () => uiRef.current?.destroy(),
2036
+ getVideoElement: () => uiRef.current?.getVideoElement() ?? null,
2037
+ }));
2038
+ return (React.createElement("div", { ref: mountRef, className: className, style: { width, height, ...style } }));
1419
2039
  });
1420
2040
  GuardVideoPlayerComponent.displayName = 'GuardVideoPlayer';
1421
2041