@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.
@@ -37281,14 +37281,14 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37281
37281
  }
37282
37282
  };
37283
37283
 
37284
- function formatTime(seconds) {
37285
- if (!isFinite(seconds) || isNaN(seconds))
37284
+ function formatTime(s) {
37285
+ if (!isFinite(s) || isNaN(s))
37286
37286
  return '0:00';
37287
- const h = Math.floor(seconds / 3600);
37288
- const m = Math.floor((seconds % 3600) / 60);
37289
- const s = Math.floor(seconds % 60);
37287
+ const h = Math.floor(s / 3600);
37288
+ const m = Math.floor((s % 3600) / 60);
37289
+ const sc = Math.floor(s % 60);
37290
37290
  const mm = String(m).padStart(h > 0 ? 2 : 1, '0');
37291
- const ss = String(s).padStart(2, '0');
37291
+ const ss = String(sc).padStart(2, '0');
37292
37292
  return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
37293
37293
  }
37294
37294
  function el(tag, cls, attrs) {
@@ -37296,15 +37296,17 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37296
37296
  if (cls)
37297
37297
  e.className = cls;
37298
37298
  if (attrs)
37299
- Object.entries(attrs).forEach(([k, v]) => e.setAttribute(k, v));
37299
+ for (const [k, v] of Object.entries(attrs))
37300
+ e.setAttribute(k, v);
37300
37301
  return e;
37301
37302
  }
37302
- function svg(path, w = 20, h = 20) {
37303
+ function svgEl(path, w = 20, h = 20) {
37303
37304
  const s = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
37304
37305
  s.setAttribute('width', String(w));
37305
37306
  s.setAttribute('height', String(h));
37306
37307
  s.setAttribute('viewBox', '0 0 24 24');
37307
37308
  s.setAttribute('fill', 'currentColor');
37309
+ s.setAttribute('aria-hidden', 'true');
37308
37310
  s.innerHTML = path;
37309
37311
  return s;
37310
37312
  }
@@ -37313,114 +37315,668 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37313
37315
  pause: '<rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/>',
37314
37316
  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"/>',
37315
37317
  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"/>',
37318
+ 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"/>',
37319
+ volLow: '<path d="M7 9v6h4l5 5V4l-5 5H7z"/>',
37316
37320
  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"/>',
37317
37321
  fsEnter: '<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>',
37318
37322
  fsExit: '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>',
37319
37323
  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"/>',
37320
- shield: '<path fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
37321
- };
37324
+ 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"/>'};
37325
+ const SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
37322
37326
  let _stylesInjected = false;
37323
- function injectStyles(accent) {
37327
+ function injectStyles() {
37324
37328
  if (_stylesInjected)
37325
37329
  return;
37326
37330
  _stylesInjected = true;
37327
37331
  const css = `
37328
- /* ── GuardVideo Custom Player ─────────────── */
37329
- .gvp-root{position:relative;background:#000;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;border-radius:10px;overflow:hidden;-webkit-user-select:none;user-select:none;outline:none;}
37330
- .gvp-root:focus-visible{box-shadow:0 0 0 3px ${accent}66;}
37331
- .gvp-video{display:block;width:100%;height:100%;object-fit:contain;border-radius:10px;}
37332
+ /* ── GuardVideo Player UI v2 ──────────────────────────────── */
37333
+
37334
+ /* Google Font import — Outfit for labels, DM Mono for time */
37335
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=DM+Mono:wght@400;500&display=swap');
37336
+
37337
+ /* ── Root ─────────────────────────────────────────────────── */
37338
+ .gvp-root {
37339
+ --gvp-accent: #00e5a0;
37340
+ --gvp-accent-dim: rgba(0, 229, 160, 0.18);
37341
+ --gvp-accent-glow: rgba(0, 229, 160, 0.35);
37342
+ --gvp-glass-bg: rgba(8, 8, 14, 0.72);
37343
+ --gvp-glass-bdr: rgba(255,255,255,0.07);
37344
+ --gvp-text: rgba(255,255,255,0.92);
37345
+ --gvp-text-dim: rgba(255,255,255,0.45);
37346
+ --gvp-radius: 12px;
37347
+ --gvp-font: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
37348
+ --gvp-mono: 'DM Mono', 'SF Mono', 'Fira Mono', monospace;
37349
+
37350
+ position: relative;
37351
+ background: #050508;
37352
+ font-family: var(--gvp-font);
37353
+ border-radius: var(--gvp-radius);
37354
+ overflow: hidden;
37355
+ -webkit-user-select: none;
37356
+ -moz-user-select: none;
37357
+ -ms-user-select: none;
37358
+ user-select: none;
37359
+ outline: none;
37360
+
37361
+ /* Subtle inner vignette for cinema depth */
37362
+ box-shadow:
37363
+ inset 0 0 80px rgba(0,0,0,0.55),
37364
+ 0 24px 80px rgba(0,0,0,0.6);
37365
+ }
37366
+
37367
+ .gvp-root:focus-visible {
37368
+ outline: 2px solid var(--gvp-accent);
37369
+ outline-offset: 2px;
37370
+ }
37371
+
37372
+ /* ── Video ────────────────────────────────────────────────── */
37373
+ .gvp-video {
37374
+ display: block;
37375
+ width: 100%;
37376
+ height: 100%;
37377
+ -o-object-fit: contain;
37378
+ object-fit: contain;
37379
+ }
37380
+
37381
+ /* ── Utility ──────────────────────────────────────────────── */
37382
+ .gvp-hidden { display: none !important; }
37383
+ .gvp-controls-hidden { opacity: 0; -webkit-transform: translateY(8px); transform: translateY(8px); pointer-events: none; }
37384
+
37385
+ /* ── Gradient overlay (top + bottom) ─────────────────────── */
37386
+ .gvp-root::before,
37387
+ .gvp-root::after {
37388
+ content: '';
37389
+ position: absolute;
37390
+ left: 0; right: 0;
37391
+ pointer-events: none;
37392
+ z-index: 2;
37393
+ }
37394
+ .gvp-root::before {
37395
+ top: 0;
37396
+ height: 90px;
37397
+ background: -webkit-linear-gradient(bottom, transparent, rgba(0,0,0,0.55));
37398
+ background: linear-gradient(to bottom, rgba(0,0,0,0.55), transparent);
37399
+ border-radius: var(--gvp-radius) var(--gvp-radius) 0 0;
37400
+ }
37401
+ .gvp-root::after {
37402
+ bottom: 0;
37403
+ height: 160px;
37404
+ background: -webkit-linear-gradient(top, transparent, rgba(0,0,0,0.88));
37405
+ background: linear-gradient(to bottom, transparent, rgba(0,0,0,0.88));
37406
+ border-radius: 0 0 var(--gvp-radius) var(--gvp-radius);
37407
+ }
37408
+
37409
+ /* ── Spinner ──────────────────────────────────────────────── */
37410
+ .gvp-spinner {
37411
+ position: absolute; inset: 0;
37412
+ display: -webkit-box; display: -ms-flexbox; display: flex;
37413
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
37414
+ -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center;
37415
+ pointer-events: none;
37416
+ z-index: 8;
37417
+ }
37418
+ .gvp-spinner-ring {
37419
+ width: 44px; height: 44px;
37420
+ border: 2.5px solid rgba(255,255,255,0.08);
37421
+ border-top-color: var(--gvp-accent);
37422
+ border-radius: 50%;
37423
+ -webkit-animation: gvp-spin 0.75s linear infinite;
37424
+ animation: gvp-spin 0.75s linear infinite;
37425
+ box-shadow: 0 0 16px var(--gvp-accent-glow);
37426
+ }
37427
+ @-webkit-keyframes gvp-spin { to { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }
37428
+ @keyframes gvp-spin { to { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }
37429
+
37430
+ /* ── Centre play overlay ──────────────────────────────────── */
37431
+ .gvp-center-play {
37432
+ position: absolute; inset: 0; z-index: 6;
37433
+ display: -webkit-box; display: -ms-flexbox; display: flex;
37434
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
37435
+ -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center;
37436
+ cursor: pointer;
37437
+ background: transparent;
37438
+ -webkit-transition: background 0.25s; transition: background 0.25s;
37439
+ border-radius: var(--gvp-radius);
37440
+ }
37441
+ .gvp-center-play:hover { background: rgba(0,0,0,0.22); }
37442
+
37443
+ .gvp-center-play-btn {
37444
+ position: relative;
37445
+ width: 76px; height: 76px;
37446
+ border-radius: 50%;
37447
+ display: -webkit-box; display: -ms-flexbox; display: flex;
37448
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
37449
+ -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center;
37450
+ color: #fff;
37451
+ /* Glass morphism */
37452
+ background: rgba(255,255,255,0.10);
37453
+ -webkit-backdrop-filter: blur(16px) saturate(180%);
37454
+ backdrop-filter: blur(16px) saturate(180%);
37455
+ border: 1px solid rgba(255,255,255,0.18);
37456
+ -webkit-transition: background 0.2s, -webkit-transform 0.2s, -webkit-box-shadow 0.2s;
37457
+ transition: background 0.2s, transform 0.2s, box-shadow 0.2s;
37458
+ box-shadow: 0 4px 32px rgba(0,0,0,0.45), 0 0 0 0 var(--gvp-accent-glow);
37459
+ }
37460
+ /* Ripple ring on hover */
37461
+ .gvp-center-play-btn::after {
37462
+ content: '';
37463
+ position: absolute; inset: -4px;
37464
+ border-radius: 50%;
37465
+ border: 1.5px solid rgba(255,255,255,0.12);
37466
+ -webkit-transition: border-color 0.2s, opacity 0.2s; transition: border-color 0.2s, opacity 0.2s;
37467
+ opacity: 0;
37468
+ }
37469
+ .gvp-center-play:hover .gvp-center-play-btn::after {
37470
+ opacity: 1;
37471
+ border-color: var(--gvp-accent);
37472
+ }
37473
+ .gvp-center-play:hover .gvp-center-play-btn {
37474
+ background: var(--gvp-accent-dim);
37475
+ -webkit-transform: scale(1.07);
37476
+ transform: scale(1.07);
37477
+ box-shadow: 0 4px 40px rgba(0,0,0,0.55), 0 0 28px var(--gvp-accent-glow);
37478
+ }
37479
+
37480
+ /* ── Click-to-toggle overlay ─────────────────────────────── */
37481
+ .gvp-click-area {
37482
+ position: absolute; inset: 0;
37483
+ cursor: pointer; z-index: 4;
37484
+ }
37485
+
37486
+ /* ── Ripple animation ─────────────────────────────────────── */
37487
+ .gvp-ripple {
37488
+ position: absolute; border-radius: 50%;
37489
+ -webkit-transform: scale(0); transform: scale(0);
37490
+ background: rgba(255,255,255,0.18);
37491
+ -webkit-animation: gvp-ripple-anim 0.55s cubic-bezier(0.22,1,0.36,1) forwards;
37492
+ animation: gvp-ripple-anim 0.55s cubic-bezier(0.22,1,0.36,1) forwards;
37493
+ pointer-events: none; z-index: 5;
37494
+ }
37495
+ @-webkit-keyframes gvp-ripple-anim { to { -webkit-transform: scale(5); transform: scale(5); opacity: 0; } }
37496
+ @keyframes gvp-ripple-anim { to { -webkit-transform: scale(5); transform: scale(5); opacity: 0; } }
37497
+
37498
+ /* ── Controls bar ─────────────────────────────────────────── */
37499
+ .gvp-controls {
37500
+ position: absolute;
37501
+ bottom: 0; left: 0; right: 0;
37502
+ z-index: 10;
37503
+ padding: 0 14px 14px;
37504
+ -webkit-transition: opacity 0.35s cubic-bezier(0.4,0,0.2,1),
37505
+ -webkit-transform 0.35s cubic-bezier(0.4,0,0.2,1);
37506
+ transition: opacity 0.35s cubic-bezier(0.4,0,0.2,1),
37507
+ transform 0.35s cubic-bezier(0.4,0,0.2,1);
37508
+ border-radius: 0 0 var(--gvp-radius) var(--gvp-radius);
37509
+ }
37510
+
37511
+ /* Inner glass pill that wraps all controls */
37512
+ .gvp-controls-inner {
37513
+ background: var(--gvp-glass-bg);
37514
+ -webkit-backdrop-filter: blur(24px) saturate(160%);
37515
+ backdrop-filter: blur(24px) saturate(160%);
37516
+ border: 1px solid var(--gvp-glass-bdr);
37517
+ border-radius: 10px;
37518
+ padding: 10px 12px 10px;
37519
+ -webkit-box-shadow: 0 8px 32px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.05);
37520
+ box-shadow: 0 8px 32px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.05);
37521
+ }
37522
+
37523
+ /* ── Seek row ─────────────────────────────────────────────── */
37524
+ .gvp-seek-row {
37525
+ display: -webkit-box; display: -ms-flexbox; display: flex;
37526
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
37527
+ gap: 8px;
37528
+ margin-bottom: 8px;
37529
+ }
37530
+
37531
+ .gvp-seek-wrap {
37532
+ -webkit-box-flex: 1; -ms-flex: 1; flex: 1;
37533
+ position: relative;
37534
+ height: 4px;
37535
+ cursor: pointer;
37536
+ padding: 8px 0;
37537
+ margin: -8px 0;
37538
+ -webkit-box-sizing: content-box; box-sizing: content-box;
37539
+ touch-action: none;
37540
+ }
37541
+
37542
+ .gvp-seek-track {
37543
+ height: 3px;
37544
+ background: rgba(255,255,255,0.12);
37545
+ border-radius: 99px;
37546
+ position: relative;
37547
+ overflow: visible;
37548
+ -webkit-transition: height 0.15s; transition: height 0.15s;
37549
+ pointer-events: none;
37550
+ }
37551
+ .gvp-seek-wrap:hover .gvp-seek-track,
37552
+ .gvp-seek-wrap.gvp-dragging .gvp-seek-track { height: 5px; }
37553
+
37554
+ .gvp-seek-buffered {
37555
+ position: absolute; left: 0; top: 0; height: 100%;
37556
+ background: rgba(255,255,255,0.22);
37557
+ border-radius: 99px;
37558
+ pointer-events: none;
37559
+ -webkit-transition: width 0.3s; transition: width 0.3s;
37560
+ }
37561
+
37562
+ .gvp-seek-progress {
37563
+ position: absolute; left: 0; top: 0; height: 100%;
37564
+ background: -webkit-linear-gradient(left, var(--gvp-accent), color-mix(in srgb, var(--gvp-accent) 80%, #fff 20%));
37565
+ background: linear-gradient(to right, var(--gvp-accent), color-mix(in srgb, var(--gvp-accent) 80%, #fff 20%));
37566
+ border-radius: 99px;
37567
+ pointer-events: none;
37568
+ /* Glow on the progress fill */
37569
+ -webkit-box-shadow: 0 0 8px var(--gvp-accent-glow);
37570
+ box-shadow: 0 0 8px var(--gvp-accent-glow);
37571
+ }
37332
37572
 
37333
- /* Spinner */
37334
- .gvp-spinner{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:none;background:rgba(0,0,0,.55);border-radius:10px;}
37335
- .gvp-spinner-ring{width:48px;height:48px;border:3px solid rgba(255,255,255,.15);border-top-color:${accent};border-radius:50%;animation:gvp-spin .8s linear infinite;}
37336
- @keyframes gvp-spin{to{transform:rotate(360deg);}}
37337
- .gvp-spinner-hidden{display:none;}
37573
+ /* Thumb — visible on hover/drag */
37574
+ .gvp-seek-thumb {
37575
+ position: absolute; top: 50%;
37576
+ -webkit-transform: translate(-50%, -50%) scale(0);
37577
+ transform: translate(-50%, -50%) scale(0);
37578
+ width: 14px; height: 14px;
37579
+ background: #fff;
37580
+ border-radius: 50%;
37581
+ pointer-events: none;
37582
+ -webkit-box-shadow: 0 0 0 3px var(--gvp-accent-dim), 0 2px 6px rgba(0,0,0,0.5);
37583
+ box-shadow: 0 0 0 3px var(--gvp-accent-dim), 0 2px 6px rgba(0,0,0,0.5);
37584
+ -webkit-transition: -webkit-transform 0.15s cubic-bezier(0.34,1.56,0.64,1);
37585
+ transition: transform 0.15s cubic-bezier(0.34,1.56,0.64,1);
37586
+ }
37587
+ .gvp-seek-wrap:hover .gvp-seek-thumb,
37588
+ .gvp-seek-wrap.gvp-dragging .gvp-seek-thumb {
37589
+ -webkit-transform: translate(-50%,-50%) scale(1);
37590
+ transform: translate(-50%,-50%) scale(1);
37591
+ }
37338
37592
 
37339
- /* Centre play */
37340
- .gvp-center-play{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;cursor:pointer;background:rgba(0,0,0,.3);transition:background .2s;border-radius:10px;z-index:3;}
37341
- .gvp-center-play:hover{background:rgba(0,0,0,.45);}
37342
- .gvp-center-play-btn{width:72px;height:72px;background:rgba(255,255,255,.12);backdrop-filter:blur(8px);border-radius:50%;display:flex;align-items:center;justify-content:center;border:2px solid rgba(255,255,255,.25);transition:background .2s,transform .15s;color:#fff;}
37343
- .gvp-center-play:hover .gvp-center-play-btn{background:${accent}cc;transform:scale(1.08);}
37344
- .gvp-center-play-hidden{display:none;}
37593
+ /* Seek tooltip */
37594
+ .gvp-seek-tooltip {
37595
+ position: absolute;
37596
+ bottom: 26px;
37597
+ -webkit-transform: translateX(-50%);
37598
+ transform: translateX(-50%);
37599
+ background: rgba(10,10,14,0.92);
37600
+ -webkit-backdrop-filter: blur(8px);
37601
+ backdrop-filter: blur(8px);
37602
+ border: 1px solid var(--gvp-glass-bdr);
37603
+ color: var(--gvp-text);
37604
+ font-family: var(--gvp-mono);
37605
+ font-size: 11px;
37606
+ font-weight: 500;
37607
+ padding: 3px 8px;
37608
+ border-radius: 5px;
37609
+ pointer-events: none;
37610
+ white-space: nowrap;
37611
+ opacity: 0;
37612
+ -webkit-transition: opacity 0.12s; transition: opacity 0.12s;
37613
+ -webkit-box-shadow: 0 4px 12px rgba(0,0,0,0.45); box-shadow: 0 4px 12px rgba(0,0,0,0.45);
37614
+ }
37615
+ .gvp-seek-wrap:hover .gvp-seek-tooltip { opacity: 1; }
37345
37616
 
37346
- /* Click overlay */
37347
- .gvp-click-area{position:absolute;inset:0;cursor:pointer;z-index:1;}
37348
- .gvp-click-area-hidden{display:none;}
37617
+ /* Caret below tooltip */
37618
+ .gvp-seek-tooltip::after {
37619
+ content: '';
37620
+ position: absolute; bottom: -5px; left: 50%;
37621
+ -webkit-transform: translateX(-50%); transform: translateX(-50%);
37622
+ border: 4px solid transparent;
37623
+ border-top-color: rgba(10,10,14,0.92);
37624
+ border-bottom-width: 0;
37625
+ }
37349
37626
 
37350
- /* Ripple */
37351
- .gvp-ripple{position:absolute;border-radius:50%;transform:scale(0);background:rgba(255,255,255,.25);animation:gvp-ripple-anim .5s ease-out forwards;pointer-events:none;z-index:2;}
37352
- @keyframes gvp-ripple-anim{to{transform:scale(4);opacity:0;}}
37627
+ /* ── Button row ───────────────────────────────────────────── */
37628
+ .gvp-btn-row {
37629
+ display: -webkit-box; display: -ms-flexbox; display: flex;
37630
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
37631
+ gap: 2px;
37632
+ }
37353
37633
 
37354
- /* Controls bar */
37355
- .gvp-controls{position:absolute;bottom:0;left:0;right:0;z-index:10;background:linear-gradient(to top,rgba(0,0,0,.85) 0%,transparent 100%);padding:36px 14px 10px;transition:opacity .3s,transform .3s;border-radius:0 0 10px 10px;}
37356
- .gvp-controls-hidden{opacity:0;transform:translateY(6px);pointer-events:none;}
37634
+ .gvp-btn {
37635
+ background: none; border: none;
37636
+ color: rgba(255,255,255,0.75);
37637
+ cursor: pointer;
37638
+ padding: 6px;
37639
+ border-radius: 7px;
37640
+ display: -webkit-inline-box; display: -ms-inline-flexbox; display: inline-flex;
37641
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
37642
+ -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center;
37643
+ -webkit-transition: background 0.14s, color 0.14s, -webkit-transform 0.12s;
37644
+ transition: background 0.14s, color 0.14s, transform 0.12s;
37645
+ -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0;
37646
+ line-height: 0;
37647
+ }
37648
+ .gvp-btn:hover {
37649
+ background: rgba(255,255,255,0.09);
37650
+ color: #fff;
37651
+ -webkit-transform: scale(1.08); transform: scale(1.08);
37652
+ }
37653
+ .gvp-btn:active {
37654
+ background: rgba(255,255,255,0.14);
37655
+ -webkit-transform: scale(0.96); transform: scale(0.96);
37656
+ }
37657
+ /* Accent highlight on play button */
37658
+ .gvp-btn-play:hover { background: var(--gvp-accent-dim); color: var(--gvp-accent); }
37357
37659
 
37358
- /* Seek bar */
37359
- .gvp-seek-row{display:flex;align-items:center;gap:8px;margin-bottom:6px;}
37360
- .gvp-seek-wrap{flex:1;position:relative;height:4px;cursor:pointer;padding:8px 0;margin:-8px 0;box-sizing:content-box;}
37361
- .gvp-seek-track{height:4px;background:rgba(255,255,255,.2);border-radius:4px;position:relative;overflow:hidden;transition:height .15s;}
37362
- .gvp-seek-wrap:hover .gvp-seek-track{height:6px;}
37363
- .gvp-seek-buffered{position:absolute;left:0;top:0;height:100%;background:rgba(255,255,255,.35);border-radius:4px;pointer-events:none;}
37364
- .gvp-seek-progress{position:absolute;left:0;top:0;height:100%;background:${accent};border-radius:4px;pointer-events:none;transition:width .1s linear;}
37365
- .gvp-seek-thumb{position:absolute;top:50%;transform:translate(-50%,-50%);width:13px;height:13px;background:#fff;border-radius:50%;box-shadow:0 1px 4px rgba(0,0,0,.5);pointer-events:none;opacity:0;transition:opacity .15s;}
37366
- .gvp-seek-wrap:hover .gvp-seek-thumb{opacity:1;}
37367
- .gvp-seek-tooltip{position:absolute;bottom:24px;transform:translateX(-50%);background:rgba(0,0,0,.8);color:#fff;font-size:11px;font-weight:500;padding:3px 7px;border-radius:4px;pointer-events:none;white-space:nowrap;opacity:0;transition:opacity .1s;}
37368
- .gvp-seek-wrap:hover .gvp-seek-tooltip{opacity:1;}
37660
+ /* ── Volume group ─────────────────────────────────────────── */
37661
+ .gvp-volume-wrap {
37662
+ display: -webkit-box; display: -ms-flexbox; display: flex;
37663
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
37664
+ gap: 2px;
37665
+ }
37666
+ .gvp-volume-slider {
37667
+ -webkit-appearance: none;
37668
+ -moz-appearance: none;
37669
+ appearance: none;
37670
+ width: 0;
37671
+ max-width: 72px;
37672
+ height: 3px;
37673
+ background: rgba(255,255,255,0.18);
37674
+ border-radius: 99px;
37675
+ outline: none;
37676
+ cursor: pointer;
37677
+ overflow: visible;
37678
+ -webkit-transition: width 0.22s cubic-bezier(0.4,0,0.2,1), opacity 0.22s;
37679
+ transition: width 0.22s cubic-bezier(0.4,0,0.2,1), opacity 0.22s;
37680
+ opacity: 0;
37681
+ /* accent-color is supported in modern browsers */
37682
+ accent-color: var(--gvp-accent);
37683
+ }
37684
+ /* Show slider when volume-wrap is hovered */
37685
+ .gvp-volume-wrap:hover .gvp-volume-slider,
37686
+ .gvp-volume-wrap:focus-within .gvp-volume-slider {
37687
+ width: 72px;
37688
+ opacity: 1;
37689
+ }
37690
+ /* WebKit thumb */
37691
+ .gvp-volume-slider::-webkit-slider-thumb {
37692
+ -webkit-appearance: none;
37693
+ width: 12px; height: 12px;
37694
+ border-radius: 50%;
37695
+ background: #fff;
37696
+ cursor: pointer;
37697
+ -webkit-box-shadow: 0 1px 4px rgba(0,0,0,0.4);
37698
+ box-shadow: 0 1px 4px rgba(0,0,0,0.4);
37699
+ }
37700
+ /* Firefox thumb */
37701
+ .gvp-volume-slider::-moz-range-thumb {
37702
+ width: 12px; height: 12px;
37703
+ border-radius: 50%;
37704
+ background: #fff;
37705
+ cursor: pointer;
37706
+ border: none;
37707
+ box-shadow: 0 1px 4px rgba(0,0,0,0.4);
37708
+ }
37709
+ /* Firefox track */
37710
+ .gvp-volume-slider::-moz-range-track {
37711
+ background: rgba(255,255,255,0.18);
37712
+ border-radius: 99px;
37713
+ height: 3px;
37714
+ }
37715
+ /* Edge/IE thumb */
37716
+ .gvp-volume-slider::-ms-thumb {
37717
+ width: 12px; height: 12px;
37718
+ border-radius: 50%;
37719
+ background: #fff;
37720
+ cursor: pointer;
37721
+ border: none;
37722
+ }
37723
+ .gvp-volume-slider::-ms-track {
37724
+ background: rgba(255,255,255,0.18);
37725
+ border-color: transparent;
37726
+ color: transparent;
37727
+ height: 3px;
37728
+ }
37729
+ .gvp-volume-slider::-ms-fill-lower { background: var(--gvp-accent); border-radius: 99px; }
37369
37730
 
37370
- /* Button row */
37371
- .gvp-btn-row{display:flex;align-items:center;gap:4px;}
37372
- .gvp-btn{background:none;border:none;color:#fff;cursor:pointer;padding:5px;border-radius:6px;display:flex;align-items:center;justify-content:center;transition:background .15s,color .15s;flex-shrink:0;}
37373
- .gvp-btn:hover{background:rgba(255,255,255,.12);color:${accent};}
37374
- .gvp-btn:active{background:rgba(255,255,255,.2);}
37731
+ /* ── Time display ──────────────────────────────────────────── */
37732
+ .gvp-time {
37733
+ font-family: var(--gvp-mono);
37734
+ font-size: 12px;
37735
+ font-weight: 500;
37736
+ color: var(--gvp-text);
37737
+ white-space: nowrap;
37738
+ letter-spacing: 0.04em;
37739
+ padding: 0 4px;
37740
+ /* Tabular numerals so digits don't shift width */
37741
+ font-variant-numeric: tabular-nums;
37742
+ -moz-font-feature-settings: "tnum";
37743
+ -webkit-font-feature-settings: "tnum";
37744
+ font-feature-settings: "tnum";
37745
+ }
37746
+ .gvp-time-sep { color: var(--gvp-text-dim); margin: 0 2px; }
37747
+
37748
+ .gvp-spacer { -webkit-box-flex: 1; -ms-flex: 1; flex: 1; }
37749
+
37750
+ /* ── Speed button ─────────────────────────────────────────── */
37751
+ .gvp-rate-label {
37752
+ font-family: var(--gvp-font);
37753
+ font-size: 11px; font-weight: 700;
37754
+ color: var(--gvp-text-dim);
37755
+ min-width: 30px; text-align: center;
37756
+ cursor: pointer;
37757
+ padding: 5px 4px;
37758
+ border-radius: 6px;
37759
+ -webkit-transition: background 0.14s, color 0.14s; transition: background 0.14s, color 0.14s;
37760
+ -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0;
37761
+ letter-spacing: 0.01em;
37762
+ line-height: 1;
37763
+ }
37764
+ .gvp-rate-label:hover { background: rgba(255,255,255,0.09); color: #fff; }
37765
+ .gvp-rate-label-active { color: var(--gvp-accent); }
37766
+
37767
+ /* ── Popup menus ──────────────────────────────────────────── */
37768
+ .gvp-menu-wrap { position: relative; }
37769
+
37770
+ .gvp-menu {
37771
+ position: absolute;
37772
+ bottom: calc(100% + 10px);
37773
+ right: 0;
37774
+ background: rgba(12,12,18,0.96);
37775
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
37776
+ backdrop-filter: blur(20px) saturate(180%);
37777
+ border: 1px solid var(--gvp-glass-bdr);
37778
+ border-radius: 10px;
37779
+ min-width: 148px;
37780
+ overflow: hidden;
37781
+ -webkit-box-shadow: 0 12px 40px rgba(0,0,0,0.65), 0 0 0 0.5px rgba(255,255,255,0.04);
37782
+ box-shadow: 0 12px 40px rgba(0,0,0,0.65), 0 0 0 0.5px rgba(255,255,255,0.04);
37783
+ -webkit-animation: gvp-menu-in 0.14s cubic-bezier(0.22,1,0.36,1);
37784
+ animation: gvp-menu-in 0.14s cubic-bezier(0.22,1,0.36,1);
37785
+ z-index: 20;
37786
+ }
37787
+ @-webkit-keyframes gvp-menu-in {
37788
+ from { opacity: 0; -webkit-transform: scale(0.92) translateY(6px); transform: scale(0.92) translateY(6px); }
37789
+ to { opacity: 1; -webkit-transform: none; transform: none; }
37790
+ }
37791
+ @keyframes gvp-menu-in {
37792
+ from { opacity: 0; -webkit-transform: scale(0.92) translateY(6px); transform: scale(0.92) translateY(6px); }
37793
+ to { opacity: 1; -webkit-transform: none; transform: none; }
37794
+ }
37795
+
37796
+ .gvp-menu-title {
37797
+ font-family: var(--gvp-font);
37798
+ font-size: 10px; font-weight: 700;
37799
+ text-transform: uppercase;
37800
+ letter-spacing: 0.12em;
37801
+ color: var(--gvp-text-dim);
37802
+ padding: 10px 13px 5px;
37803
+ }
37804
+
37805
+ .gvp-menu-item {
37806
+ display: -webkit-box; display: -ms-flexbox; display: flex;
37807
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
37808
+ -webkit-box-pack: justify; -ms-flex-pack: justify; justify-content: space-between;
37809
+ padding: 7px 13px;
37810
+ font-family: var(--gvp-font);
37811
+ font-size: 13px; font-weight: 500;
37812
+ color: var(--gvp-text);
37813
+ cursor: pointer;
37814
+ -webkit-transition: background 0.1s; transition: background 0.1s;
37815
+ gap: 12px;
37816
+ }
37817
+ .gvp-menu-item:hover { background: rgba(255,255,255,0.06); }
37818
+ .gvp-menu-item-active { color: var(--gvp-accent); }
37819
+
37820
+ .gvp-menu-check {
37821
+ font-size: 13px;
37822
+ color: var(--gvp-accent);
37823
+ line-height: 1;
37824
+ /* Unicode checkmark — crisp on all platforms */
37825
+ }
37826
+
37827
+ .gvp-menu-sep {
37828
+ height: 1px;
37829
+ background: rgba(255,255,255,0.06);
37830
+ margin: 3px 0;
37831
+ }
37832
+
37833
+ /* ── Error overlay ────────────────────────────────────────── */
37834
+ .gvp-error {
37835
+ position: absolute; inset: 0; z-index: 15;
37836
+ display: -webkit-box; display: -ms-flexbox; display: flex;
37837
+ -webkit-box-orient: vertical; -webkit-box-direction: normal;
37838
+ -ms-flex-direction: column; flex-direction: column;
37839
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
37840
+ -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center;
37841
+ background: rgba(6,6,10,0.88);
37842
+ -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
37843
+ color: #fff;
37844
+ gap: 10px;
37845
+ padding: 28px;
37846
+ border-radius: var(--gvp-radius);
37847
+ text-align: center;
37848
+ }
37849
+ .gvp-error-icon { font-size: 40px; line-height: 1; }
37850
+ .gvp-error-code {
37851
+ font-family: var(--gvp-mono);
37852
+ font-size: 10px; font-weight: 500;
37853
+ letter-spacing: 0.14em;
37854
+ color: #f87171;
37855
+ text-transform: uppercase;
37856
+ background: rgba(248,113,113,0.1);
37857
+ border: 1px solid rgba(248,113,113,0.25);
37858
+ padding: 3px 10px; border-radius: 4px;
37859
+ }
37860
+ .gvp-error-msg {
37861
+ font-family: var(--gvp-font);
37862
+ font-size: 14px; font-weight: 400;
37863
+ color: rgba(255,255,255,0.6);
37864
+ max-width: 320px; line-height: 1.6;
37865
+ }
37375
37866
 
37376
- /* Volume */
37377
- .gvp-volume-wrap{display:flex;align-items:center;gap:5px;}
37378
- .gvp-volume-slider{-webkit-appearance:none;width:70px;height:4px;background:rgba(255,255,255,.25);border-radius:4px;outline:none;cursor:pointer;accent-color:${accent};}
37379
- .gvp-volume-slider::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:#fff;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,.4);}
37380
- .gvp-volume-slider::-moz-range-thumb{width:12px;height:12px;border-radius:50%;background:#fff;cursor:pointer;border:none;}
37867
+ /* ── Secure badge ─────────────────────────────────────────── */
37868
+ .gvp-badge {
37869
+ position: absolute; top: 12px; right: 14px; z-index: 5;
37870
+ display: -webkit-inline-box; display: -ms-inline-flexbox; display: inline-flex;
37871
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
37872
+ gap: 5px;
37873
+ background: rgba(8,8,14,0.60);
37874
+ -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
37875
+ border: 1px solid rgba(255,255,255,0.08);
37876
+ border-radius: 99px;
37877
+ padding: 4px 10px 4px 7px;
37878
+ font-family: var(--gvp-font);
37879
+ font-size: 10px; font-weight: 700;
37880
+ color: var(--gvp-accent);
37881
+ pointer-events: none;
37882
+ letter-spacing: 0.06em;
37883
+ text-transform: uppercase;
37884
+ -webkit-transition: opacity 0.3s; transition: opacity 0.3s;
37885
+ -webkit-box-shadow: 0 2px 12px rgba(0,0,0,0.4), 0 0 0 0.5px rgba(255,255,255,0.05);
37886
+ box-shadow: 0 2px 12px rgba(0,0,0,0.4), 0 0 0 0.5px rgba(255,255,255,0.05);
37887
+ }
37888
+ .gvp-badge-hidden { opacity: 0; }
37381
37889
 
37382
- /* Time */
37383
- .gvp-time{font-size:12px;color:rgba(255,255,255,.85);font-variant-numeric:tabular-nums;white-space:nowrap;letter-spacing:.02em;}
37384
- .gvp-spacer{flex:1;}
37890
+ /* ── Forensic watermark ───────────────────────────────────── */
37891
+ .gvp-watermark {
37892
+ position: absolute; inset: 0;
37893
+ pointer-events: none; overflow: hidden; z-index: 6;
37894
+ }
37895
+ .gvp-watermark-text {
37896
+ position: absolute;
37897
+ white-space: nowrap;
37898
+ font-family: var(--gvp-mono);
37899
+ font-size: 12px;
37900
+ color: rgba(255,255,255,0.055);
37901
+ -webkit-transform: rotate(-28deg); transform: rotate(-28deg);
37902
+ -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none;
37903
+ pointer-events: none;
37904
+ letter-spacing: 0.06em;
37905
+ }
37385
37906
 
37386
- /* Menus */
37387
- .gvp-menu-wrap{position:relative;}
37388
- .gvp-menu{position:absolute;bottom:calc(100% + 8px);right:0;background:rgba(18,18,22,.95);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,.08);border-radius:8px;min-width:140px;overflow:hidden;box-shadow:0 8px 24px rgba(0,0,0,.5);animation:gvp-menu-in .1s ease-out;z-index:20;}
37389
- .gvp-menu-hidden{display:none;}
37390
- @keyframes gvp-menu-in{from{opacity:0;transform:scale(.95) translateY(4px);}to{opacity:1;transform:scale(1) translateY(0);}}
37391
- .gvp-menu-title{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:rgba(255,255,255,.4);padding:8px 12px 4px;}
37392
- .gvp-menu-item{display:flex;align-items:center;justify-content:space-between;padding:7px 12px;font-size:13px;color:rgba(255,255,255,.85);cursor:pointer;transition:background .12s;gap:10px;}
37393
- .gvp-menu-item:hover{background:rgba(255,255,255,.07);}
37394
- .gvp-menu-item-active{color:${accent};font-weight:600;}
37395
- .gvp-menu-check{font-size:14px;color:${accent};}
37396
- .gvp-menu-sep{height:1px;background:rgba(255,255,255,.07);margin:3px 0;}
37907
+ /* ── Live dot (for live streams) ─────────────────────────── */
37908
+ .gvp-live-badge {
37909
+ display: -webkit-inline-box; display: -ms-inline-flexbox; display: inline-flex;
37910
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
37911
+ gap: 5px;
37912
+ font-family: var(--gvp-font);
37913
+ font-size: 11px; font-weight: 700;
37914
+ color: #f87171;
37915
+ letter-spacing: 0.08em;
37916
+ text-transform: uppercase;
37917
+ }
37918
+ .gvp-live-dot {
37919
+ width: 7px; height: 7px;
37920
+ background: #f87171;
37921
+ border-radius: 50%;
37922
+ -webkit-animation: gvp-live-pulse 1.5s ease-in-out infinite;
37923
+ animation: gvp-live-pulse 1.5s ease-in-out infinite;
37924
+ }
37925
+ @-webkit-keyframes gvp-live-pulse {
37926
+ 0%, 100% { opacity: 1; -webkit-transform: scale(1); transform: scale(1); }
37927
+ 50% { opacity: 0.5; -webkit-transform: scale(0.7); transform: scale(0.7); }
37928
+ }
37929
+ @keyframes gvp-live-pulse {
37930
+ 0%, 100% { opacity: 1; transform: scale(1); }
37931
+ 50% { opacity: 0.5; transform: scale(0.7); }
37932
+ }
37397
37933
 
37398
- /* Error */
37399
- .gvp-error{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(0,0,0,.8);color:#fff;gap:10px;padding:24px;border-radius:10px;text-align:center;}
37400
- .gvp-error-hidden{display:none;}
37401
- .gvp-error-icon{font-size:36px;}
37402
- .gvp-error-code{font-size:11px;font-weight:700;letter-spacing:.1em;color:#f87171;text-transform:uppercase;}
37403
- .gvp-error-msg{font-size:14px;color:rgba(255,255,255,.7);max-width:320px;line-height:1.5;}
37934
+ /* ── Divider between button groups ───────────────────────── */
37935
+ .gvp-divider {
37936
+ width: 1px; height: 18px;
37937
+ background: rgba(255,255,255,0.1);
37938
+ margin: 0 4px;
37939
+ -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0;
37940
+ }
37404
37941
 
37405
- /* Secure badge */
37406
- .gvp-badge{position:absolute;top:10px;right:12px;display:flex;align-items:center;gap:5px;background:rgba(0,0,0,.55);backdrop-filter:blur(6px);border-radius:20px;padding:3px 9px 3px 7px;font-size:10px;font-weight:600;color:${accent};pointer-events:none;z-index:5;letter-spacing:.04em;transition:opacity .3s;}
37407
- .gvp-badge-hidden{opacity:0;}
37942
+ /* ── Tooltip on buttons ───────────────────────────────────── */
37943
+ .gvp-btn[title]:hover::after {
37944
+ content: attr(title);
37945
+ position: absolute;
37946
+ bottom: calc(100% + 7px);
37947
+ left: 50%; -webkit-transform: translateX(-50%); transform: translateX(-50%);
37948
+ background: rgba(10,10,14,0.95);
37949
+ border: 1px solid var(--gvp-glass-bdr);
37950
+ color: var(--gvp-text);
37951
+ font-family: var(--gvp-font);
37952
+ font-size: 11px; font-weight: 500;
37953
+ padding: 3px 8px; border-radius: 5px;
37954
+ white-space: nowrap;
37955
+ pointer-events: none;
37956
+ z-index: 30;
37957
+ }
37408
37958
 
37409
- /* Watermark */
37410
- .gvp-watermark{position:absolute;inset:0;pointer-events:none;overflow:hidden;z-index:6;}
37411
- .gvp-watermark-text{position:absolute;white-space:nowrap;font-size:13px;font-family:monospace;color:rgba(255,255,255,.065);transform:rotate(-28deg);user-select:none;pointer-events:none;letter-spacing:.05em;}
37959
+ /* ── Focus ring (keyboard nav) ────────────────────────────── */
37960
+ .gvp-btn:focus-visible,
37961
+ .gvp-rate-label:focus-visible,
37962
+ .gvp-seek-wrap:focus-visible {
37963
+ outline: 2px solid var(--gvp-accent);
37964
+ outline-offset: 2px;
37965
+ }
37412
37966
 
37413
- /* Speed */
37414
- .gvp-rate-label{font-size:11px;font-weight:700;color:rgba(255,255,255,.7);min-width:28px;text-align:center;cursor:pointer;padding:5px 4px;border-radius:4px;transition:background .12s;flex-shrink:0;}
37415
- .gvp-rate-label:hover{background:rgba(255,255,255,.1);}
37967
+ /* ── Responsive: hide volume slider text on tiny players ─── */
37968
+ @media (max-width: 380px) {
37969
+ .gvp-volume-slider { display: none; }
37970
+ .gvp-time { font-size: 11px; }
37971
+ .gvp-controls-inner { padding: 8px 10px; }
37972
+ }
37416
37973
  `;
37417
37974
  const tag = document.createElement('style');
37418
- tag.setAttribute('data-guardvideo', 'player-ui-styles');
37975
+ tag.setAttribute('data-guardvideo', 'player-ui-styles-v2');
37419
37976
  tag.textContent = css;
37420
37977
  document.head.appendChild(tag);
37421
37978
  }
37422
- const SPEEDS = [0.5, 0.75, 1, 1.25, 1.5, 2];
37423
- class VanillaPlayerUI {
37979
+ class PlayerUI {
37424
37980
  constructor(container, videoId, config) {
37425
37981
  this.playerState = exports.PlayerState.IDLE;
37426
37982
  this.duration = 0;
@@ -37430,198 +37986,285 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37430
37986
  this.playbackRate = 1;
37431
37987
  this.openMenu = null;
37432
37988
  this.hideTimer = null;
37433
- this.watermarkText = null;
37434
37989
  this.seekDragging = false;
37435
- this.accent = config.branding?.accentColor ?? '#44c09b';
37436
- this.brandName = config.branding?.name ?? 'GuardVideo';
37437
- const forensic = config.forensicWatermark !== false;
37438
- injectStyles(this.accent);
37439
- this.buildDOM(container, config.width ?? '100%', config.height ?? 'auto');
37440
- this.wireEvents(videoId, config, forensic);
37441
- if (forensic) {
37442
- const wtxt = config.viewerEmail || config.viewerName || '';
37443
- if (wtxt) {
37444
- setTimeout(() => {
37445
- this.watermarkText = wtxt;
37446
- this.renderWatermark();
37447
- }, 1500);
37448
- }
37449
- }
37450
- }
37451
- buildDOM(container, width, height) {
37990
+ const accent = config.branding?.accentColor ?? '#00e5a0';
37991
+ const brandName = config.branding?.name ?? 'GuardVideo';
37992
+ injectStyles();
37452
37993
  container.innerHTML = '';
37453
37994
  this.root = el('div', 'gvp-root');
37454
- this.root.style.width = width;
37455
- this.root.style.height = height;
37995
+ this.root.style.width = config.width ?? '100%';
37996
+ this.root.style.height = config.height ?? 'auto';
37997
+ this.root.style.setProperty('--gvp-accent', accent);
37998
+ this.root.style.setProperty('--gvp-accent-dim', this._hexToRgba(accent, 0.18));
37999
+ this.root.style.setProperty('--gvp-accent-glow', this._hexToRgba(accent, 0.35));
37456
38000
  this.root.setAttribute('tabindex', '0');
38001
+ this.root.setAttribute('role', 'region');
38002
+ this.root.setAttribute('aria-label', `${brandName} video player`);
37457
38003
  this.videoEl = el('video', 'gvp-video', { playsinline: '', preload: 'metadata' });
37458
38004
  this.badge = el('div', 'gvp-badge');
37459
- const shieldSvg = svg(ICON.shield, 14, 14);
37460
- shieldSvg.style.color = this.accent;
37461
- this.badge.appendChild(shieldSvg);
37462
- this.badge.appendChild(document.createTextNode(this.brandName));
38005
+ this.badge.setAttribute('aria-hidden', 'true');
38006
+ this.badge.appendChild(svgEl(ICON.shield, 13, 13));
38007
+ this.badge.appendChild(document.createTextNode(brandName));
37463
38008
  this.watermarkDiv = el('div', 'gvp-watermark');
37464
- this.spinner = el('div', 'gvp-spinner gvp-spinner-hidden');
38009
+ this.watermarkDiv.setAttribute('aria-hidden', 'true');
38010
+ this.spinner = el('div', 'gvp-spinner gvp-hidden');
38011
+ this.spinner.setAttribute('aria-label', 'Loading');
38012
+ this.spinner.setAttribute('role', 'status');
37465
38013
  this.spinner.appendChild(el('div', 'gvp-spinner-ring'));
37466
- this.errorOverlay = el('div', 'gvp-error gvp-error-hidden');
38014
+ this.errorOverlay = el('div', 'gvp-error gvp-hidden');
38015
+ this.errorOverlay.setAttribute('role', 'alert');
37467
38016
  const errIcon = el('div', 'gvp-error-icon');
37468
38017
  errIcon.textContent = '⚠️';
38018
+ errIcon.setAttribute('aria-hidden', 'true');
37469
38019
  const errCode = el('div', 'gvp-error-code');
37470
- errCode.id = 'gvp-err-code';
37471
38020
  const errMsg = el('div', 'gvp-error-msg');
37472
- errMsg.id = 'gvp-err-msg';
37473
38021
  this.errorOverlay.append(errIcon, errCode, errMsg);
37474
38022
  this.centerPlay = el('div', 'gvp-center-play');
38023
+ this.centerPlay.setAttribute('role', 'button');
38024
+ this.centerPlay.setAttribute('aria-label', 'Play');
37475
38025
  this.centerPlayBtn = el('div', 'gvp-center-play-btn');
37476
- this.centerPlayBtn.appendChild(svg(ICON.play, 32, 32));
38026
+ this.centerPlayBtn.appendChild(svgEl(ICON.play, 30, 30));
37477
38027
  this.centerPlay.appendChild(this.centerPlayBtn);
37478
- this.clickArea = el('div', 'gvp-click-area gvp-click-area-hidden');
38028
+ this.clickArea = el('div', 'gvp-click-area gvp-hidden');
38029
+ this.clickArea.setAttribute('aria-hidden', 'true');
37479
38030
  this.controls = el('div', 'gvp-controls');
38031
+ const inner = el('div', 'gvp-controls-inner');
37480
38032
  const seekRow = el('div', 'gvp-seek-row');
37481
38033
  this.seekWrap = el('div', 'gvp-seek-wrap');
38034
+ this.seekWrap.setAttribute('role', 'slider');
38035
+ this.seekWrap.setAttribute('aria-label', 'Seek');
38036
+ this.seekWrap.setAttribute('aria-valuemin', '0');
38037
+ this.seekWrap.setAttribute('aria-valuemax', '100');
38038
+ this.seekWrap.setAttribute('aria-valuenow', '0');
38039
+ this.seekWrap.setAttribute('aria-valuetext', '0:00 of 0:00');
38040
+ this.seekWrap.setAttribute('tabindex', '0');
37482
38041
  const seekTrack = el('div', 'gvp-seek-track');
37483
38042
  this.seekBuffered = el('div', 'gvp-seek-buffered');
37484
38043
  this.seekProgress = el('div', 'gvp-seek-progress');
37485
38044
  this.seekThumb = el('div', 'gvp-seek-thumb');
37486
38045
  this.seekTooltip = el('div', 'gvp-seek-tooltip');
38046
+ this.seekTooltip.setAttribute('aria-hidden', 'true');
37487
38047
  seekTrack.append(this.seekBuffered, this.seekProgress);
37488
38048
  this.seekWrap.append(seekTrack, this.seekThumb, this.seekTooltip);
37489
38049
  seekRow.appendChild(this.seekWrap);
37490
- this.controls.appendChild(seekRow);
38050
+ inner.appendChild(seekRow);
37491
38051
  const btnRow = el('div', 'gvp-btn-row');
37492
- this.playBtn = el('button', 'gvp-btn');
38052
+ this.playBtn = el('button', 'gvp-btn gvp-btn-play');
38053
+ this.playBtn.type = 'button';
38054
+ this.playBtn.setAttribute('aria-label', 'Play');
37493
38055
  this.playBtn.title = 'Play (k)';
37494
- this.playBtn.appendChild(svg(ICON.play));
38056
+ this.playBtn.appendChild(svgEl(ICON.play));
37495
38057
  const volWrap = el('div', 'gvp-volume-wrap');
37496
38058
  this.volBtn = el('button', 'gvp-btn');
38059
+ this.volBtn.type = 'button';
38060
+ this.volBtn.setAttribute('aria-label', 'Mute');
37497
38061
  this.volBtn.title = 'Mute (m)';
37498
- this.volBtn.appendChild(svg(ICON.volHigh, 18, 18));
38062
+ this.volBtn.appendChild(svgEl(ICON.volHigh, 18, 18));
37499
38063
  this.volSlider = el('input', 'gvp-volume-slider');
37500
- this.volSlider.type = 'range';
37501
- this.volSlider.min = '0';
37502
- this.volSlider.max = '1';
37503
- this.volSlider.step = '0.02';
37504
- this.volSlider.value = '1';
37505
- this.volSlider.title = 'Volume';
38064
+ Object.assign(this.volSlider, { type: 'range', min: '0', max: '1', step: '0.02', value: '1' });
38065
+ this.volSlider.setAttribute('aria-label', 'Volume');
37506
38066
  volWrap.append(this.volBtn, this.volSlider);
37507
38067
  this.timeEl = el('span', 'gvp-time');
38068
+ this.timeEl.setAttribute('aria-live', 'off');
37508
38069
  this.timeEl.textContent = '0:00 / 0:00';
37509
38070
  const spacer = el('div', 'gvp-spacer');
38071
+ const divider = el('div', 'gvp-divider');
38072
+ divider.setAttribute('aria-hidden', 'true');
37510
38073
  const speedWrap = el('div', 'gvp-menu-wrap');
37511
38074
  this.speedLabel = el('span', 'gvp-rate-label');
37512
38075
  this.speedLabel.textContent = '1×';
37513
- this.speedLabel.title = 'Playback speed';
37514
- this.speedMenu = el('div', 'gvp-menu gvp-menu-hidden');
38076
+ this.speedLabel.setAttribute('role', 'button');
38077
+ this.speedLabel.setAttribute('aria-label', 'Playback speed');
38078
+ this.speedLabel.setAttribute('aria-haspopup', 'menu');
38079
+ this.speedLabel.setAttribute('tabindex', '0');
38080
+ this.speedMenu = el('div', 'gvp-menu gvp-hidden');
38081
+ this.speedMenu.setAttribute('role', 'menu');
38082
+ this.speedMenu.setAttribute('aria-label', 'Playback speed');
37515
38083
  const speedTitle = el('div', 'gvp-menu-title');
38084
+ speedTitle.setAttribute('aria-hidden', 'true');
37516
38085
  speedTitle.textContent = 'Speed';
37517
38086
  this.speedMenu.appendChild(speedTitle);
37518
38087
  SPEEDS.forEach(r => {
37519
- const item = el('div', `gvp-menu-item${r === 1 ? ' gvp-menu-item-active' : ''}`);
37520
- item.textContent = r === 1 ? 'Normal' : `${r}×`;
38088
+ const active = r === 1;
38089
+ const item = el('div', active ? 'gvp-menu-item gvp-menu-item-active' : 'gvp-menu-item');
38090
+ item.setAttribute('role', 'menuitemradio');
38091
+ item.setAttribute('aria-checked', active ? 'true' : 'false');
37521
38092
  item.dataset['speed'] = String(r);
38093
+ item.textContent = r === 1 ? 'Normal' : `${r}×`;
38094
+ if (active) {
38095
+ const check = el('span', 'gvp-menu-check');
38096
+ check.setAttribute('aria-hidden', 'true');
38097
+ check.textContent = '✓';
38098
+ item.appendChild(check);
38099
+ }
37522
38100
  this.speedMenu.appendChild(item);
37523
38101
  });
37524
38102
  speedWrap.append(this.speedLabel, this.speedMenu);
37525
38103
  const qualWrap = el('div', 'gvp-menu-wrap');
37526
38104
  this.settingsBtn = el('button', 'gvp-btn');
37527
- this.settingsBtn.title = 'Quality settings';
37528
- this.settingsBtn.appendChild(svg(ICON.settings, 18, 18));
37529
- this.settingsBtn.style.display = 'none';
37530
- this.qualityMenu = el('div', 'gvp-menu gvp-menu-hidden');
38105
+ this.settingsBtn.type = 'button';
38106
+ this.settingsBtn.setAttribute('aria-label', 'Quality settings');
38107
+ this.settingsBtn.setAttribute('aria-haspopup', 'menu');
38108
+ this.settingsBtn.title = 'Quality';
38109
+ this.settingsBtn.appendChild(svgEl(ICON.settings, 18, 18));
38110
+ this.settingsBtn.classList.add('gvp-hidden');
38111
+ this.qualityMenu = el('div', 'gvp-menu gvp-hidden');
38112
+ this.qualityMenu.setAttribute('role', 'menu');
38113
+ this.qualityMenu.setAttribute('aria-label', 'Video quality');
37531
38114
  qualWrap.append(this.settingsBtn, this.qualityMenu);
37532
38115
  this.fsBtn = el('button', 'gvp-btn');
38116
+ this.fsBtn.type = 'button';
38117
+ this.fsBtn.setAttribute('aria-label', 'Enter fullscreen');
37533
38118
  this.fsBtn.title = 'Fullscreen (f)';
37534
- this.fsBtn.appendChild(svg(ICON.fsEnter, 18, 18));
37535
- btnRow.append(this.playBtn, volWrap, this.timeEl, spacer, speedWrap, qualWrap, this.fsBtn);
37536
- this.controls.appendChild(btnRow);
38119
+ this.fsBtn.appendChild(svgEl(ICON.fsEnter, 18, 18));
38120
+ btnRow.append(this.playBtn, volWrap, this.timeEl, spacer, speedWrap, divider, qualWrap, this.fsBtn);
38121
+ inner.appendChild(btnRow);
38122
+ this.controls.appendChild(inner);
37537
38123
  this.root.append(this.videoEl, this.badge, this.watermarkDiv, this.spinner, this.errorOverlay, this.centerPlay, this.clickArea, this.controls);
37538
38124
  container.appendChild(this.root);
37539
- }
37540
- wireEvents(videoId, config, forensicWatermark) {
38125
+ this._onFsChangeBound = () => this._onFsChange();
38126
+ this._seekMouseMoveBound = (e) => { if (this.seekDragging)
38127
+ this._seekTo(e.clientX); };
38128
+ this._seekMouseUpBound = () => this._endSeekDrag();
38129
+ this._seekTouchMoveBound = (e) => {
38130
+ if (!this.seekDragging)
38131
+ return;
38132
+ e.preventDefault();
38133
+ this._seekTo(e.touches[0].clientX);
38134
+ };
38135
+ this._seekTouchEndBound = () => this._endSeekDrag();
38136
+ this._wireEvents(videoId, config);
38137
+ if (config.forensicWatermark !== false) {
38138
+ const wmText = config.viewerEmail || config.viewerName || '';
38139
+ if (wmText)
38140
+ this._renderWatermark(wmText);
38141
+ }
38142
+ }
38143
+ _hexToRgba(hex, alpha) {
38144
+ const clean = hex.replace('#', '');
38145
+ const full = clean.length === 3
38146
+ ? clean.split('').map(c => c + c).join('')
38147
+ : clean;
38148
+ const r = parseInt(full.substring(0, 2), 16);
38149
+ const g = parseInt(full.substring(2, 4), 16);
38150
+ const b = parseInt(full.substring(4, 6), 16);
38151
+ if (isNaN(r) || isNaN(g) || isNaN(b))
38152
+ return `rgba(0,229,160,${alpha})`;
38153
+ return `rgba(${r},${g},${b},${alpha})`;
38154
+ }
38155
+ _wireEvents(videoId, config) {
37541
38156
  const video = this.videoEl;
37542
38157
  this.corePlayer = new GuardVideoPlayer$1(video, videoId, {
37543
38158
  ...config,
37544
38159
  controls: false,
37545
- forensicWatermark,
38160
+ forensicWatermark: config.forensicWatermark !== false,
37546
38161
  onReady: () => {
37547
38162
  config.onReady?.();
37548
- setTimeout(() => {
37549
- this.qualityLevels = this.corePlayer.getQualityLevels();
37550
- this.buildQualityMenu();
37551
- }, 100);
38163
+ const levels = this.corePlayer.getQualityLevels();
38164
+ if (levels.length) {
38165
+ this.qualityLevels = levels;
38166
+ this._buildQualityMenu();
38167
+ }
38168
+ else {
38169
+ setTimeout(() => {
38170
+ this.qualityLevels = this.corePlayer.getQualityLevels();
38171
+ if (this.qualityLevels.length)
38172
+ this._buildQualityMenu();
38173
+ }, 100);
38174
+ }
37552
38175
  },
37553
38176
  onError: (err) => {
37554
- this.showError(err);
38177
+ this._showError(err);
37555
38178
  config.onError?.(err);
37556
38179
  },
37557
38180
  onQualityChange: (quality) => {
37558
38181
  const idx = this.qualityLevels.findIndex(l => l.name === quality);
37559
38182
  this.currentQualityIdx = idx;
37560
- this.refreshQualityMenu();
38183
+ this._refreshQualityMenu();
37561
38184
  config.onQualityChange?.(quality);
37562
38185
  },
37563
38186
  onStateChange: (state) => {
37564
- this.onStateChange(state);
38187
+ this._onStateChange(state);
37565
38188
  config.onStateChange?.(state);
37566
38189
  },
37567
38190
  });
37568
- video.addEventListener('timeupdate', () => this.onTimeUpdate());
37569
- video.addEventListener('loadedmetadata', () => { this.duration = video.duration || 0; this.onTimeUpdate(); });
37570
- video.addEventListener('durationchange', () => { this.duration = video.duration || 0; });
38191
+ video.addEventListener('timeupdate', () => {
38192
+ config.onTimeUpdate?.(video.currentTime);
38193
+ this._onTimeUpdate();
38194
+ });
38195
+ video.addEventListener('loadedmetadata', () => {
38196
+ this.duration = video.duration || 0;
38197
+ this._onTimeUpdate();
38198
+ });
38199
+ video.addEventListener('durationchange', () => {
38200
+ this.duration = video.duration || 0;
38201
+ });
37571
38202
  video.addEventListener('progress', () => {
37572
38203
  if (video.buffered.length > 0) {
37573
38204
  this.bufferedEnd = video.buffered.end(video.buffered.length - 1);
37574
- this.updateSeekBar();
38205
+ this._updateSeekBar();
37575
38206
  }
37576
38207
  });
37577
- video.addEventListener('volumechange', () => this.onVolumeChange());
38208
+ video.addEventListener('volumechange', () => this._onVolumeChange());
37578
38209
  video.addEventListener('ended', () => {
37579
- this.showControls(true);
37580
- this.renderPlayBtn();
38210
+ config.onEnded?.();
38211
+ this._showControls(true);
38212
+ this._renderPlayBtn();
37581
38213
  });
37582
38214
  video.addEventListener('ratechange', () => {
37583
38215
  this.playbackRate = video.playbackRate;
37584
- this.speedLabel.textContent = this.playbackRate === 1 ? '1×' : `${this.playbackRate}×`;
38216
+ const isNormal = this.playbackRate === 1;
38217
+ this.speedLabel.textContent = isNormal ? '1×' : `${this.playbackRate}×`;
38218
+ this.speedLabel.classList.toggle('gvp-rate-label-active', !isNormal);
37585
38219
  });
37586
- this.playBtn.addEventListener('click', (e) => { e.stopPropagation(); this.togglePlay(); });
37587
- this.volBtn.addEventListener('click', (e) => { e.stopPropagation(); this.toggleMute(); });
38220
+ this.playBtn.addEventListener('click', (e) => { e.stopPropagation(); this._togglePlay(); });
38221
+ this.volBtn.addEventListener('click', (e) => { e.stopPropagation(); this._toggleMute(); });
37588
38222
  this.volSlider.addEventListener('input', () => {
37589
38223
  video.volume = parseFloat(this.volSlider.value);
37590
38224
  video.muted = video.volume === 0;
37591
38225
  });
37592
- this.seekWrap.addEventListener('click', (e) => {
37593
- e.stopPropagation();
37594
- this.seekTo(e);
37595
- this.resetHideTimer();
37596
- });
37597
- this.seekWrap.addEventListener('mousemove', (e) => this.onSeekHover(e));
37598
- this.seekWrap.addEventListener('mouseleave', () => { this.seekTooltip.style.opacity = '0'; });
37599
38226
  this.seekWrap.addEventListener('mousedown', (e) => {
37600
- this.seekDragging = true;
37601
- this.seekTo(e);
37602
- const onMove = (ev) => { if (this.seekDragging)
37603
- this.seekTo(ev); };
37604
- const onUp = () => { this.seekDragging = false; window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); };
37605
- window.addEventListener('mousemove', onMove);
37606
- window.addEventListener('mouseup', onUp);
38227
+ e.preventDefault();
38228
+ this._startSeekDrag();
38229
+ this._seekTo(e.clientX);
38230
+ window.addEventListener('mousemove', this._seekMouseMoveBound);
38231
+ window.addEventListener('mouseup', this._seekMouseUpBound);
37607
38232
  });
37608
- this.speedLabel.addEventListener('click', (e) => {
37609
- e.stopPropagation();
37610
- this.toggleMenu('speed');
38233
+ this.seekWrap.addEventListener('mousemove', (e) => this._onSeekHover(e.clientX));
38234
+ this.seekWrap.addEventListener('mouseleave', () => {
38235
+ this.seekTooltip.style.opacity = '0';
38236
+ });
38237
+ this.seekWrap.addEventListener('touchstart', (e) => {
38238
+ e.preventDefault();
38239
+ this._startSeekDrag();
38240
+ this._seekTo(e.touches[0].clientX);
38241
+ window.addEventListener('touchmove', this._seekTouchMoveBound, { passive: false });
38242
+ window.addEventListener('touchend', this._seekTouchEndBound);
38243
+ }, { passive: false });
38244
+ this.seekWrap.addEventListener('keydown', (e) => {
38245
+ if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight')
38246
+ return;
38247
+ e.preventDefault();
38248
+ const delta = e.key === 'ArrowLeft' ? -5 : 5;
38249
+ this.corePlayer.seek(Math.max(0, Math.min(this.duration, video.currentTime + delta)));
38250
+ this._resetHideTimer();
38251
+ });
38252
+ this.speedLabel.addEventListener('click', (e) => { e.stopPropagation(); this._toggleMenu('speed'); });
38253
+ this.speedLabel.addEventListener('keydown', (e) => {
38254
+ if (e.key === 'Enter' || e.key === ' ') {
38255
+ e.preventDefault();
38256
+ this._toggleMenu('speed');
38257
+ }
37611
38258
  });
37612
38259
  this.speedMenu.addEventListener('click', (e) => {
37613
38260
  e.stopPropagation();
37614
38261
  const item = e.target.closest('[data-speed]');
37615
38262
  if (!item)
37616
38263
  return;
37617
- const r = parseFloat(item.dataset['speed']);
37618
- video.playbackRate = r;
37619
- this.closeMenus();
37620
- });
37621
- this.settingsBtn.addEventListener('click', (e) => {
37622
- e.stopPropagation();
37623
- this.toggleMenu('quality');
38264
+ video.playbackRate = parseFloat(item.dataset['speed']);
38265
+ this._closeMenus();
37624
38266
  });
38267
+ this.settingsBtn.addEventListener('click', (e) => { e.stopPropagation(); this._toggleMenu('quality'); });
37625
38268
  this.qualityMenu.addEventListener('click', (e) => {
37626
38269
  e.stopPropagation();
37627
38270
  const item = e.target.closest('[data-quality]');
@@ -37630,132 +38273,122 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37630
38273
  const idx = parseInt(item.dataset['quality']);
37631
38274
  this.corePlayer.setQuality(idx);
37632
38275
  this.currentQualityIdx = idx;
37633
- this.refreshQualityMenu();
37634
- this.closeMenus();
37635
- });
37636
- this.clickArea.addEventListener('click', (e) => {
37637
- this.addRipple(e);
37638
- this.togglePlay();
38276
+ this._refreshQualityMenu();
38277
+ this._closeMenus();
37639
38278
  });
37640
- this.centerPlay.addEventListener('click', (e) => { e.stopPropagation(); this.togglePlay(); });
37641
- this.fsBtn.addEventListener('click', (e) => { e.stopPropagation(); this.toggleFullscreen(); });
37642
- document.addEventListener('fullscreenchange', () => this.onFsChange());
37643
- this.root.addEventListener('mousemove', () => this.resetHideTimer());
38279
+ this.clickArea.addEventListener('click', (e) => { this._addRipple(e); this._togglePlay(); });
38280
+ this.centerPlay.addEventListener('click', (e) => { e.stopPropagation(); this._togglePlay(); });
38281
+ this.fsBtn.addEventListener('click', (e) => { e.stopPropagation(); this._toggleFullscreen(); });
38282
+ document.addEventListener('fullscreenchange', this._onFsChangeBound);
38283
+ document.addEventListener('webkitfullscreenchange', this._onFsChangeBound);
38284
+ document.addEventListener('mozfullscreenchange', this._onFsChangeBound);
38285
+ document.addEventListener('MSFullscreenChange', this._onFsChangeBound);
38286
+ this.root.addEventListener('mousemove', () => this._resetHideTimer());
38287
+ this.root.addEventListener('touchstart', () => this._resetHideTimer(), { passive: true });
37644
38288
  this.root.addEventListener('mouseleave', () => {
37645
38289
  if (this.playerState === exports.PlayerState.PLAYING)
37646
- this.showControls(false);
37647
- });
37648
- this.root.addEventListener('click', () => this.closeMenus());
37649
- this.root.addEventListener('keydown', (e) => {
37650
- switch (e.code) {
37651
- case 'Space':
37652
- case 'KeyK':
37653
- e.preventDefault();
37654
- this.togglePlay();
37655
- break;
37656
- case 'ArrowLeft':
37657
- e.preventDefault();
37658
- this.corePlayer.seek(Math.max(0, video.currentTime - 5));
37659
- break;
37660
- case 'ArrowRight':
37661
- e.preventDefault();
37662
- this.corePlayer.seek(Math.min(this.duration, video.currentTime + 5));
37663
- break;
37664
- case 'ArrowUp':
37665
- e.preventDefault();
37666
- video.volume = Math.min(1, video.volume + 0.1);
37667
- break;
37668
- case 'ArrowDown':
37669
- e.preventDefault();
37670
- video.volume = Math.max(0, video.volume - 0.1);
37671
- break;
37672
- case 'KeyM':
37673
- e.preventDefault();
37674
- this.toggleMute();
37675
- break;
37676
- case 'KeyF':
37677
- e.preventDefault();
37678
- this.toggleFullscreen();
37679
- break;
37680
- }
38290
+ this._showControls(false);
37681
38291
  });
38292
+ this.root.addEventListener('click', () => this._closeMenus());
38293
+ this.root.addEventListener('keydown', (e) => this._onKeyDown(e));
37682
38294
  }
37683
- onStateChange(state) {
38295
+ _onStateChange(state) {
37684
38296
  this.playerState = state;
37685
38297
  const loading = state === exports.PlayerState.LOADING || state === exports.PlayerState.BUFFERING;
37686
38298
  const idle = state === exports.PlayerState.IDLE;
37687
38299
  const ended = this.videoEl.ended;
37688
- this.spinner.classList.toggle('gvp-spinner-hidden', !loading);
38300
+ this.spinner.classList.toggle('gvp-hidden', !loading);
37689
38301
  const showCenter = (idle || ended) && !loading;
37690
- this.centerPlay.classList.toggle('gvp-center-play-hidden', !showCenter);
38302
+ this.centerPlay.classList.toggle('gvp-hidden', !showCenter);
37691
38303
  if (showCenter) {
37692
38304
  this.centerPlayBtn.innerHTML = '';
37693
- this.centerPlayBtn.appendChild(svg(ended ? ICON.replay : ICON.play, 32, 32));
38305
+ this.centerPlayBtn.appendChild(svgEl(ended ? ICON.replay : ICON.play, 30, 30));
38306
+ this.centerPlay.setAttribute('aria-label', ended ? 'Replay' : 'Play');
37694
38307
  }
37695
- this.clickArea.classList.toggle('gvp-click-area-hidden', idle);
38308
+ this.clickArea.classList.toggle('gvp-hidden', idle);
37696
38309
  if (state !== exports.PlayerState.PLAYING) {
37697
- this.showControls(true);
38310
+ this._showControls(true);
37698
38311
  if (this.hideTimer) {
37699
38312
  clearTimeout(this.hideTimer);
37700
38313
  this.hideTimer = null;
37701
38314
  }
37702
38315
  }
37703
38316
  else {
37704
- this.resetHideTimer();
38317
+ this._resetHideTimer();
37705
38318
  }
37706
- this.renderPlayBtn();
38319
+ this._renderPlayBtn();
37707
38320
  }
37708
- togglePlay() {
37709
- const video = this.videoEl;
37710
- if (video.paused || video.ended) {
38321
+ _togglePlay() {
38322
+ const v = this.videoEl;
38323
+ if (v.paused || v.ended) {
37711
38324
  this.corePlayer.play().catch(() => { });
37712
38325
  }
37713
38326
  else {
37714
38327
  this.corePlayer.pause();
37715
38328
  }
37716
- this.resetHideTimer();
38329
+ this._resetHideTimer();
37717
38330
  }
37718
- renderPlayBtn() {
37719
- const video = this.videoEl;
37720
- const ended = video.ended;
38331
+ _renderPlayBtn() {
38332
+ const ended = this.videoEl.ended;
37721
38333
  const playing = this.playerState === exports.PlayerState.PLAYING;
38334
+ const icon = ended ? ICON.replay : playing ? ICON.pause : ICON.play;
38335
+ const label = playing ? 'Pause' : 'Play';
37722
38336
  this.playBtn.innerHTML = '';
37723
- this.playBtn.appendChild(svg(ended ? ICON.replay : playing ? ICON.pause : ICON.play));
37724
- this.playBtn.title = playing ? 'Pause (k)' : 'Play (k)';
37725
- }
37726
- toggleMute() {
37727
- this.videoEl.muted = !this.videoEl.muted;
37728
- }
37729
- onVolumeChange() {
37730
- const video = this.videoEl;
37731
- const muted = video.muted || video.volume === 0;
38337
+ this.playBtn.appendChild(svgEl(icon));
38338
+ this.playBtn.setAttribute('aria-label', label);
38339
+ this.playBtn.title = `${label} (k)`;
38340
+ }
38341
+ _toggleMute() { this.videoEl.muted = !this.videoEl.muted; }
38342
+ _onVolumeChange() {
38343
+ const v = this.videoEl;
38344
+ const vol = v.muted ? 0 : v.volume;
38345
+ const muted = vol === 0;
38346
+ const icon = muted ? ICON.volMute : vol < 0.4 ? ICON.volLow : vol < 0.75 ? ICON.volMid : ICON.volHigh;
37732
38347
  this.volBtn.innerHTML = '';
37733
- this.volBtn.appendChild(svg(muted ? ICON.volMute : ICON.volHigh, 18, 18));
37734
- this.volSlider.value = String(muted ? 0 : video.volume);
38348
+ this.volBtn.appendChild(svgEl(icon, 18, 18));
38349
+ this.volBtn.setAttribute('aria-label', muted ? 'Unmute' : 'Mute');
38350
+ this.volBtn.title = muted ? 'Unmute (m)' : 'Mute (m)';
38351
+ this.volSlider.value = String(vol);
38352
+ }
38353
+ _startSeekDrag() {
38354
+ this.seekDragging = true;
38355
+ this.seekWrap.classList.add('gvp-dragging');
37735
38356
  }
37736
- seekTo(e) {
38357
+ _endSeekDrag() {
38358
+ this.seekDragging = false;
38359
+ this.seekWrap.classList.remove('gvp-dragging');
38360
+ window.removeEventListener('mousemove', this._seekMouseMoveBound);
38361
+ window.removeEventListener('mouseup', this._seekMouseUpBound);
38362
+ window.removeEventListener('touchmove', this._seekTouchMoveBound);
38363
+ window.removeEventListener('touchend', this._seekTouchEndBound);
38364
+ this._resetHideTimer();
38365
+ }
38366
+ _seekTo(clientX) {
37737
38367
  if (!this.duration)
37738
38368
  return;
37739
38369
  const rect = this.seekWrap.getBoundingClientRect();
37740
- const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
38370
+ const pct = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
37741
38371
  this.corePlayer.seek(pct * this.duration);
37742
38372
  }
37743
- onSeekHover(e) {
38373
+ _onSeekHover(clientX) {
37744
38374
  if (!this.duration)
37745
38375
  return;
37746
38376
  const rect = this.seekWrap.getBoundingClientRect();
37747
- const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
38377
+ const pct = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
37748
38378
  this.seekTooltip.textContent = formatTime(pct * this.duration);
37749
38379
  this.seekTooltip.style.left = `${pct * 100}%`;
37750
38380
  this.seekTooltip.style.opacity = '1';
37751
38381
  }
37752
- onTimeUpdate() {
37753
- const video = this.videoEl;
37754
- const ct = video.currentTime;
37755
- this.timeEl.textContent = `${formatTime(ct)} / ${formatTime(this.duration)}`;
37756
- this.updateSeekBar();
38382
+ _onTimeUpdate() {
38383
+ const ct = this.videoEl.currentTime;
38384
+ const dur = this.duration;
38385
+ this.timeEl.textContent = `${formatTime(ct)} / ${formatTime(dur)}`;
38386
+ this._updateSeekBar();
38387
+ const pct = dur > 0 ? Math.round((ct / dur) * 100) : 0;
38388
+ this.seekWrap.setAttribute('aria-valuenow', String(pct));
38389
+ this.seekWrap.setAttribute('aria-valuetext', `${formatTime(ct)} of ${formatTime(dur)}`);
37757
38390
  }
37758
- updateSeekBar() {
38391
+ _updateSeekBar() {
37759
38392
  const ct = this.videoEl.currentTime;
37760
38393
  const dur = this.duration;
37761
38394
  if (!dur)
@@ -37766,128 +38399,196 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37766
38399
  this.seekBuffered.style.width = `${bPct}%`;
37767
38400
  this.seekThumb.style.left = `${pPct}%`;
37768
38401
  }
37769
- buildQualityMenu() {
38402
+ _buildQualityMenu() {
37770
38403
  if (!this.qualityLevels.length)
37771
38404
  return;
37772
- this.settingsBtn.style.display = '';
38405
+ this.settingsBtn.classList.remove('gvp-hidden');
37773
38406
  this.qualityMenu.innerHTML = '';
37774
38407
  const title = el('div', 'gvp-menu-title');
38408
+ title.setAttribute('aria-hidden', 'true');
37775
38409
  title.textContent = 'Quality';
37776
38410
  const autoItem = el('div', 'gvp-menu-item gvp-menu-item-active');
38411
+ autoItem.setAttribute('role', 'menuitemradio');
38412
+ autoItem.setAttribute('aria-checked', 'true');
37777
38413
  autoItem.dataset['quality'] = '-1';
37778
38414
  autoItem.textContent = 'Auto';
37779
- const check = el('span', 'gvp-menu-check');
37780
- check.textContent = '';
37781
- autoItem.appendChild(check);
37782
- const sep = el('div', 'gvp-menu-sep');
37783
- this.qualityMenu.append(title, autoItem, sep);
38415
+ const checkEl = el('span', 'gvp-menu-check');
38416
+ checkEl.setAttribute('aria-hidden', 'true');
38417
+ checkEl.textContent = '✓';
38418
+ autoItem.appendChild(checkEl);
38419
+ this.qualityMenu.append(title, autoItem, el('div', 'gvp-menu-sep'));
37784
38420
  [...this.qualityLevels].reverse().forEach(q => {
37785
38421
  const item = el('div', 'gvp-menu-item');
38422
+ item.setAttribute('role', 'menuitemradio');
38423
+ item.setAttribute('aria-checked', 'false');
37786
38424
  item.dataset['quality'] = String(q.index);
37787
38425
  item.textContent = q.name;
37788
38426
  this.qualityMenu.appendChild(item);
37789
38427
  });
37790
38428
  this.currentQualityIdx = -1;
37791
38429
  }
37792
- refreshQualityMenu() {
38430
+ _refreshQualityMenu() {
37793
38431
  this.qualityMenu.querySelectorAll('[data-quality]').forEach(item => {
37794
- const idx = parseInt(item.dataset['quality']);
37795
- const active = idx === this.currentQualityIdx;
37796
- item.className = `gvp-menu-item${active ? ' gvp-menu-item-active' : ''}`;
38432
+ const active = parseInt(item.dataset['quality']) === this.currentQualityIdx;
38433
+ item.className = active ? 'gvp-menu-item gvp-menu-item-active' : 'gvp-menu-item';
38434
+ item.setAttribute('aria-checked', String(active));
37797
38435
  const existing = item.querySelector('.gvp-menu-check');
37798
38436
  if (active && !existing) {
37799
- const check = el('span', 'gvp-menu-check');
37800
- check.textContent = '';
37801
- item.appendChild(check);
38437
+ const c = el('span', 'gvp-menu-check');
38438
+ c.setAttribute('aria-hidden', 'true');
38439
+ c.textContent = '✓';
38440
+ item.appendChild(c);
37802
38441
  }
37803
38442
  else if (!active && existing) {
37804
38443
  existing.remove();
37805
38444
  }
37806
38445
  });
37807
38446
  }
37808
- refreshSpeedMenu() {
38447
+ _refreshSpeedMenu() {
37809
38448
  this.speedMenu.querySelectorAll('[data-speed]').forEach(item => {
37810
- const r = parseFloat(item.dataset['speed']);
37811
- item.className = `gvp-menu-item${r === this.playbackRate ? ' gvp-menu-item-active' : ''}`;
38449
+ const active = parseFloat(item.dataset['speed']) === this.playbackRate;
38450
+ item.className = active ? 'gvp-menu-item gvp-menu-item-active' : 'gvp-menu-item';
38451
+ item.setAttribute('aria-checked', String(active));
38452
+ const existing = item.querySelector('.gvp-menu-check');
38453
+ if (active && !existing) {
38454
+ const c = el('span', 'gvp-menu-check');
38455
+ c.setAttribute('aria-hidden', 'true');
38456
+ c.textContent = '✓';
38457
+ item.appendChild(c);
38458
+ }
38459
+ else if (!active && existing) {
38460
+ existing.remove();
38461
+ }
37812
38462
  });
37813
38463
  }
37814
- toggleMenu(which) {
38464
+ _toggleMenu(which) {
37815
38465
  const same = this.openMenu === which;
37816
- this.closeMenus();
38466
+ this._closeMenus();
37817
38467
  if (!same) {
37818
38468
  this.openMenu = which;
37819
38469
  if (which === 'speed') {
37820
- this.refreshSpeedMenu();
37821
- this.speedMenu.classList.remove('gvp-menu-hidden');
38470
+ this._refreshSpeedMenu();
38471
+ this.speedMenu.classList.remove('gvp-hidden');
37822
38472
  }
37823
38473
  else {
37824
- this.refreshQualityMenu();
37825
- this.qualityMenu.classList.remove('gvp-menu-hidden');
38474
+ this._refreshQualityMenu();
38475
+ this.qualityMenu.classList.remove('gvp-hidden');
37826
38476
  }
37827
38477
  }
37828
38478
  }
37829
- closeMenus() {
38479
+ _closeMenus() {
37830
38480
  this.openMenu = null;
37831
- this.speedMenu.classList.add('gvp-menu-hidden');
37832
- this.qualityMenu.classList.add('gvp-menu-hidden');
38481
+ this.speedMenu.classList.add('gvp-hidden');
38482
+ this.qualityMenu.classList.add('gvp-hidden');
37833
38483
  }
37834
- showControls(visible) {
38484
+ _showControls(visible) {
37835
38485
  this.controls.classList.toggle('gvp-controls-hidden', !visible);
37836
38486
  this.badge.classList.toggle('gvp-badge-hidden', !visible);
37837
38487
  }
37838
- resetHideTimer() {
37839
- this.showControls(true);
38488
+ _resetHideTimer() {
38489
+ this._showControls(true);
37840
38490
  if (this.hideTimer)
37841
38491
  clearTimeout(this.hideTimer);
37842
38492
  this.hideTimer = setTimeout(() => {
37843
- if (this.playerState === exports.PlayerState.PLAYING)
37844
- this.showControls(false);
38493
+ if (this.playerState === exports.PlayerState.PLAYING && !this.openMenu) {
38494
+ this._showControls(false);
38495
+ }
37845
38496
  }, 2800);
37846
38497
  }
37847
- toggleFullscreen() {
37848
- if (document.fullscreenElement) {
37849
- document.exitFullscreen().catch(() => { });
38498
+ _toggleFullscreen() {
38499
+ const doc = document;
38500
+ const root = this.root;
38501
+ const isFs = !!(document.fullscreenElement ||
38502
+ doc.webkitFullscreenElement ||
38503
+ doc.mozFullScreenElement ||
38504
+ doc.msFullscreenElement);
38505
+ if (isFs) {
38506
+ (document.exitFullscreen?.() ||
38507
+ doc.webkitExitFullscreen?.() ||
38508
+ doc.mozCancelFullScreen?.() ||
38509
+ (doc.msExitFullscreen?.(), Promise.resolve()))
38510
+ ?.catch?.(() => { });
37850
38511
  }
37851
38512
  else {
37852
- this.root.requestFullscreen().catch(() => { });
37853
- }
37854
- }
37855
- onFsChange() {
37856
- const fs = !!document.fullscreenElement;
38513
+ (root.requestFullscreen?.() ||
38514
+ root.webkitRequestFullscreen?.() ||
38515
+ root.mozRequestFullScreen?.() ||
38516
+ (root.msRequestFullscreen?.(), Promise.resolve()))
38517
+ ?.catch?.(() => { });
38518
+ }
38519
+ }
38520
+ _onFsChange() {
38521
+ const doc = document;
38522
+ const fs = !!(document.fullscreenElement ||
38523
+ doc.webkitFullscreenElement ||
38524
+ doc.mozFullScreenElement ||
38525
+ doc.msFullscreenElement);
37857
38526
  this.fsBtn.innerHTML = '';
37858
- this.fsBtn.appendChild(svg(fs ? ICON.fsExit : ICON.fsEnter, 18, 18));
37859
- this.fsBtn.title = fs ? 'Exit fullscreen (f)' : 'Fullscreen (f)';
37860
- }
37861
- showError(err) {
37862
- const codeEl = this.root.querySelector('#gvp-err-code');
37863
- const msgEl = this.root.querySelector('#gvp-err-msg');
37864
- if (codeEl)
37865
- codeEl.textContent = err.code;
37866
- if (msgEl)
37867
- msgEl.textContent = err.message;
37868
- this.errorOverlay.classList.remove('gvp-error-hidden');
37869
- this.controls.style.display = 'none';
37870
- }
37871
- renderWatermark() {
37872
- if (!this.watermarkText)
37873
- return;
38527
+ this.fsBtn.appendChild(svgEl(fs ? ICON.fsExit : ICON.fsEnter, 18, 18));
38528
+ this.fsBtn.setAttribute('aria-label', fs ? 'Exit fullscreen' : 'Enter fullscreen');
38529
+ this.fsBtn.title = fs ? 'Exit fullscreen (f)' : 'Enter fullscreen (f)';
38530
+ }
38531
+ _showError(err) {
38532
+ this.errorOverlay.children[1].textContent = err.code;
38533
+ this.errorOverlay.children[2].textContent = err.message;
38534
+ this.errorOverlay.classList.remove('gvp-hidden');
38535
+ this.controls.classList.add('gvp-hidden');
38536
+ }
38537
+ _renderWatermark(text) {
37874
38538
  this.watermarkDiv.innerHTML = '';
37875
38539
  for (let i = 0; i < 20; i++) {
37876
38540
  const span = el('span', 'gvp-watermark-text');
37877
- span.textContent = this.watermarkText;
38541
+ span.textContent = text;
37878
38542
  span.style.left = `${(i % 4) * 26 + (Math.floor(i / 4) % 2) * 13}%`;
37879
38543
  span.style.top = `${Math.floor(i / 4) * 22}%`;
37880
38544
  this.watermarkDiv.appendChild(span);
37881
38545
  }
37882
38546
  }
37883
- addRipple(e) {
38547
+ _addRipple(e) {
37884
38548
  const rect = this.root.getBoundingClientRect();
37885
38549
  const d = document.createElement('div');
37886
38550
  d.className = 'gvp-ripple';
37887
- const size = 60;
37888
- 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`;
38551
+ const sz = 60;
38552
+ 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`;
37889
38553
  this.root.appendChild(d);
37890
- setTimeout(() => d.remove(), 600);
38554
+ setTimeout(() => d.remove(), 650);
38555
+ }
38556
+ _onKeyDown(e) {
38557
+ switch (e.code) {
38558
+ case 'Space':
38559
+ case 'KeyK':
38560
+ e.preventDefault();
38561
+ this._togglePlay();
38562
+ break;
38563
+ case 'ArrowLeft':
38564
+ if (document.activeElement !== this.seekWrap) {
38565
+ e.preventDefault();
38566
+ this.corePlayer.seek(Math.max(0, this.videoEl.currentTime - 5));
38567
+ }
38568
+ break;
38569
+ case 'ArrowRight':
38570
+ if (document.activeElement !== this.seekWrap) {
38571
+ e.preventDefault();
38572
+ this.corePlayer.seek(Math.min(this.duration, this.videoEl.currentTime + 5));
38573
+ }
38574
+ break;
38575
+ case 'ArrowUp':
38576
+ e.preventDefault();
38577
+ this.videoEl.volume = Math.min(1, this.videoEl.volume + 0.1);
38578
+ break;
38579
+ case 'ArrowDown':
38580
+ e.preventDefault();
38581
+ this.videoEl.volume = Math.max(0, this.videoEl.volume - 0.1);
38582
+ break;
38583
+ case 'KeyM':
38584
+ e.preventDefault();
38585
+ this._toggleMute();
38586
+ break;
38587
+ case 'KeyF':
38588
+ e.preventDefault();
38589
+ this._toggleFullscreen();
38590
+ break;
38591
+ }
37891
38592
  }
37892
38593
  play() { return this.corePlayer.play(); }
37893
38594
  pause() { return this.corePlayer.pause(); }
@@ -37904,22 +38605,32 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37904
38605
  destroy() {
37905
38606
  if (this.hideTimer)
37906
38607
  clearTimeout(this.hideTimer);
38608
+ document.removeEventListener('fullscreenchange', this._onFsChangeBound);
38609
+ document.removeEventListener('webkitfullscreenchange', this._onFsChangeBound);
38610
+ document.removeEventListener('mozfullscreenchange', this._onFsChangeBound);
38611
+ document.removeEventListener('MSFullscreenChange', this._onFsChangeBound);
38612
+ window.removeEventListener('mousemove', this._seekMouseMoveBound);
38613
+ window.removeEventListener('mouseup', this._seekMouseUpBound);
38614
+ window.removeEventListener('touchmove', this._seekTouchMoveBound);
38615
+ window.removeEventListener('touchend', this._seekTouchEndBound);
37907
38616
  this.corePlayer.destroy();
37908
38617
  this.root.remove();
37909
38618
  }
37910
38619
  }
38620
+
37911
38621
  class GuardVideoPlayer {
37912
38622
  constructor(ui) {
37913
38623
  this.ui = ui;
37914
38624
  }
37915
38625
  static create(containerId, videoId, config) {
37916
38626
  const container = document.getElementById(containerId);
37917
- if (!container)
37918
- throw new Error(`Container "#${containerId}" not found`);
37919
- return new GuardVideoPlayer(new VanillaPlayerUI(container, videoId, config));
38627
+ if (!container) {
38628
+ throw new Error(`GuardVideoPlayer: element '#${containerId}' not found`);
38629
+ }
38630
+ return new GuardVideoPlayer(new PlayerUI(container, videoId, config));
37920
38631
  }
37921
38632
  static createInElement(container, videoId, config) {
37922
- return new GuardVideoPlayer(new VanillaPlayerUI(container, videoId, config));
38633
+ return new GuardVideoPlayer(new PlayerUI(container, videoId, config));
37923
38634
  }
37924
38635
  play() { return this.ui.play(); }
37925
38636
  pause() { return this.ui.pause(); }
@@ -37941,6 +38652,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37941
38652
  }
37942
38653
 
37943
38654
  exports.GuardVideoPlayer = GuardVideoPlayer;
38655
+ exports.PlayerUI = PlayerUI;
37944
38656
 
37945
38657
  return exports;
37946
38658