@guardvideo/player-sdk 1.0.2 → 1.2.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.
@@ -36688,7 +36688,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36688
36688
  security: { ...DEFAULT_SECURITY, ...config.security },
36689
36689
  viewerName: config.viewerName || '',
36690
36690
  viewerEmail: config.viewerEmail || '',
36691
- forensicWatermark: config.forensicWatermark || false,
36691
+ forensicWatermark: config.forensicWatermark !== false,
36692
36692
  onReady: config.onReady || (() => { }),
36693
36693
  onError: config.onError || (() => { }),
36694
36694
  onQualityChange: config.onQualityChange || (() => { }),
@@ -37083,7 +37083,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37083
37083
  maxViews: null,
37084
37084
  ...(this.config.viewerName ? { viewerName: this.config.viewerName } : {}),
37085
37085
  ...(this.config.viewerEmail ? { viewerEmail: this.config.viewerEmail } : {}),
37086
- ...(this.config.forensicWatermark ? { forensicWatermark: this.config.forensicWatermark } : {}),
37086
+ forensicWatermark: this.config.forensicWatermark,
37087
37087
  }),
37088
37088
  });
37089
37089
  if (!response.ok) {
@@ -37281,33 +37281,659 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37281
37281
  }
37282
37282
  };
37283
37283
 
37284
- class GuardVideoPlayer extends GuardVideoPlayer$1 {
37285
- static create(containerId, videoId, config) {
37286
- const container = document.getElementById(containerId);
37287
- if (!container) {
37288
- throw new Error(`Container element with id "${containerId}" not found`);
37289
- }
37290
- const videoElement = document.createElement('video');
37291
- videoElement.setAttribute('playsinline', '');
37292
- videoElement.setAttribute('preload', 'metadata');
37293
- videoElement.style.width = '100%';
37294
- videoElement.style.height = '100%';
37295
- videoElement.style.objectFit = 'contain';
37284
+ function formatTime(seconds) {
37285
+ if (!isFinite(seconds) || isNaN(seconds))
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);
37290
+ const mm = String(m).padStart(h > 0 ? 2 : 1, '0');
37291
+ const ss = String(s).padStart(2, '0');
37292
+ return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`;
37293
+ }
37294
+ function el(tag, cls, attrs) {
37295
+ const e = document.createElement(tag);
37296
+ if (cls)
37297
+ e.className = cls;
37298
+ if (attrs)
37299
+ Object.entries(attrs).forEach(([k, v]) => e.setAttribute(k, v));
37300
+ return e;
37301
+ }
37302
+ function svg(path, w = 20, h = 20) {
37303
+ const s = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
37304
+ s.setAttribute('width', String(w));
37305
+ s.setAttribute('height', String(h));
37306
+ s.setAttribute('viewBox', '0 0 24 24');
37307
+ s.setAttribute('fill', 'currentColor');
37308
+ s.innerHTML = path;
37309
+ return s;
37310
+ }
37311
+ const ICON = {
37312
+ play: '<polygon points="5,3 19,12 5,21"/>',
37313
+ pause: '<rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/>',
37314
+ 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
+ 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"/>',
37316
+ 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
+ fsEnter: '<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>',
37318
+ fsExit: '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>',
37319
+ 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
+ };
37322
+ let _stylesInjected = false;
37323
+ function injectStyles(accent) {
37324
+ if (_stylesInjected)
37325
+ return;
37326
+ _stylesInjected = true;
37327
+ 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
+
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;}
37338
+
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;}
37345
+
37346
+ /* Click overlay */
37347
+ .gvp-click-area{position:absolute;inset:0;cursor:pointer;z-index:1;}
37348
+ .gvp-click-area-hidden{display:none;}
37349
+
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;}}
37353
+
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;}
37357
+
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;}
37369
+
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);}
37375
+
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;}
37381
+
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;}
37385
+
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;}
37397
+
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;}
37404
+
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;}
37408
+
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;}
37412
+
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);}
37416
+ `;
37417
+ const tag = document.createElement('style');
37418
+ tag.setAttribute('data-guardvideo', 'player-ui-styles');
37419
+ tag.textContent = css;
37420
+ document.head.appendChild(tag);
37421
+ }
37422
+ const SPEEDS = [0.5, 0.75, 1, 1.25, 1.5, 2];
37423
+ class VanillaPlayerUI {
37424
+ constructor(container, videoId, config) {
37425
+ this.playerState = exports.PlayerState.IDLE;
37426
+ this.duration = 0;
37427
+ this.bufferedEnd = 0;
37428
+ this.qualityLevels = [];
37429
+ this.currentQualityIdx = -1;
37430
+ this.playbackRate = 1;
37431
+ this.openMenu = null;
37432
+ this.hideTimer = null;
37433
+ this.watermarkText = null;
37434
+ 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) {
37296
37452
  container.innerHTML = '';
37297
- container.style.position = 'relative';
37298
- container.style.backgroundColor = '#000';
37299
- container.appendChild(videoElement);
37300
- return new GuardVideoPlayer(videoElement, videoId, config);
37453
+ this.root = el('div', 'gvp-root');
37454
+ this.root.style.width = width;
37455
+ this.root.style.height = height;
37456
+ this.root.setAttribute('tabindex', '0');
37457
+ this.videoEl = el('video', 'gvp-video', { playsinline: '', preload: 'metadata' });
37458
+ 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));
37463
+ this.watermarkDiv = el('div', 'gvp-watermark');
37464
+ this.spinner = el('div', 'gvp-spinner gvp-spinner-hidden');
37465
+ this.spinner.appendChild(el('div', 'gvp-spinner-ring'));
37466
+ this.errorOverlay = el('div', 'gvp-error gvp-error-hidden');
37467
+ const errIcon = el('div', 'gvp-error-icon');
37468
+ errIcon.textContent = '⚠️';
37469
+ const errCode = el('div', 'gvp-error-code');
37470
+ errCode.id = 'gvp-err-code';
37471
+ const errMsg = el('div', 'gvp-error-msg');
37472
+ errMsg.id = 'gvp-err-msg';
37473
+ this.errorOverlay.append(errIcon, errCode, errMsg);
37474
+ this.centerPlay = el('div', 'gvp-center-play');
37475
+ this.centerPlayBtn = el('div', 'gvp-center-play-btn');
37476
+ this.centerPlayBtn.appendChild(svg(ICON.play, 32, 32));
37477
+ this.centerPlay.appendChild(this.centerPlayBtn);
37478
+ this.clickArea = el('div', 'gvp-click-area gvp-click-area-hidden');
37479
+ this.controls = el('div', 'gvp-controls');
37480
+ const seekRow = el('div', 'gvp-seek-row');
37481
+ this.seekWrap = el('div', 'gvp-seek-wrap');
37482
+ const seekTrack = el('div', 'gvp-seek-track');
37483
+ this.seekBuffered = el('div', 'gvp-seek-buffered');
37484
+ this.seekProgress = el('div', 'gvp-seek-progress');
37485
+ this.seekThumb = el('div', 'gvp-seek-thumb');
37486
+ this.seekTooltip = el('div', 'gvp-seek-tooltip');
37487
+ seekTrack.append(this.seekBuffered, this.seekProgress);
37488
+ this.seekWrap.append(seekTrack, this.seekThumb, this.seekTooltip);
37489
+ seekRow.appendChild(this.seekWrap);
37490
+ this.controls.appendChild(seekRow);
37491
+ const btnRow = el('div', 'gvp-btn-row');
37492
+ this.playBtn = el('button', 'gvp-btn');
37493
+ this.playBtn.title = 'Play (k)';
37494
+ this.playBtn.appendChild(svg(ICON.play));
37495
+ const volWrap = el('div', 'gvp-volume-wrap');
37496
+ this.volBtn = el('button', 'gvp-btn');
37497
+ this.volBtn.title = 'Mute (m)';
37498
+ this.volBtn.appendChild(svg(ICON.volHigh, 18, 18));
37499
+ 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';
37506
+ volWrap.append(this.volBtn, this.volSlider);
37507
+ this.timeEl = el('span', 'gvp-time');
37508
+ this.timeEl.textContent = '0:00 / 0:00';
37509
+ const spacer = el('div', 'gvp-spacer');
37510
+ const speedWrap = el('div', 'gvp-menu-wrap');
37511
+ this.speedLabel = el('span', 'gvp-rate-label');
37512
+ this.speedLabel.textContent = '1×';
37513
+ this.speedLabel.title = 'Playback speed';
37514
+ this.speedMenu = el('div', 'gvp-menu gvp-menu-hidden');
37515
+ const speedTitle = el('div', 'gvp-menu-title');
37516
+ speedTitle.textContent = 'Speed';
37517
+ this.speedMenu.appendChild(speedTitle);
37518
+ SPEEDS.forEach(r => {
37519
+ const item = el('div', `gvp-menu-item${r === 1 ? ' gvp-menu-item-active' : ''}`);
37520
+ item.textContent = r === 1 ? 'Normal' : `${r}×`;
37521
+ item.dataset['speed'] = String(r);
37522
+ this.speedMenu.appendChild(item);
37523
+ });
37524
+ speedWrap.append(this.speedLabel, this.speedMenu);
37525
+ const qualWrap = el('div', 'gvp-menu-wrap');
37526
+ 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');
37531
+ qualWrap.append(this.settingsBtn, this.qualityMenu);
37532
+ this.fsBtn = el('button', 'gvp-btn');
37533
+ 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);
37537
+ this.root.append(this.videoEl, this.badge, this.watermarkDiv, this.spinner, this.errorOverlay, this.centerPlay, this.clickArea, this.controls);
37538
+ container.appendChild(this.root);
37539
+ }
37540
+ wireEvents(videoId, config, forensicWatermark) {
37541
+ const video = this.videoEl;
37542
+ this.corePlayer = new GuardVideoPlayer$1(video, videoId, {
37543
+ ...config,
37544
+ controls: false,
37545
+ forensicWatermark,
37546
+ onReady: () => {
37547
+ config.onReady?.();
37548
+ setTimeout(() => {
37549
+ this.qualityLevels = this.corePlayer.getQualityLevels();
37550
+ this.buildQualityMenu();
37551
+ }, 100);
37552
+ },
37553
+ onError: (err) => {
37554
+ this.showError(err);
37555
+ config.onError?.(err);
37556
+ },
37557
+ onQualityChange: (quality) => {
37558
+ const idx = this.qualityLevels.findIndex(l => l.name === quality);
37559
+ this.currentQualityIdx = idx;
37560
+ this.refreshQualityMenu();
37561
+ config.onQualityChange?.(quality);
37562
+ },
37563
+ onStateChange: (state) => {
37564
+ this.onStateChange(state);
37565
+ config.onStateChange?.(state);
37566
+ },
37567
+ });
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; });
37571
+ video.addEventListener('progress', () => {
37572
+ if (video.buffered.length > 0) {
37573
+ this.bufferedEnd = video.buffered.end(video.buffered.length - 1);
37574
+ this.updateSeekBar();
37575
+ }
37576
+ });
37577
+ video.addEventListener('volumechange', () => this.onVolumeChange());
37578
+ video.addEventListener('ended', () => {
37579
+ this.showControls(true);
37580
+ this.renderPlayBtn();
37581
+ });
37582
+ video.addEventListener('ratechange', () => {
37583
+ this.playbackRate = video.playbackRate;
37584
+ this.speedLabel.textContent = this.playbackRate === 1 ? '1×' : `${this.playbackRate}×`;
37585
+ });
37586
+ this.playBtn.addEventListener('click', (e) => { e.stopPropagation(); this.togglePlay(); });
37587
+ this.volBtn.addEventListener('click', (e) => { e.stopPropagation(); this.toggleMute(); });
37588
+ this.volSlider.addEventListener('input', () => {
37589
+ video.volume = parseFloat(this.volSlider.value);
37590
+ video.muted = video.volume === 0;
37591
+ });
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
+ 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);
37607
+ });
37608
+ this.speedLabel.addEventListener('click', (e) => {
37609
+ e.stopPropagation();
37610
+ this.toggleMenu('speed');
37611
+ });
37612
+ this.speedMenu.addEventListener('click', (e) => {
37613
+ e.stopPropagation();
37614
+ const item = e.target.closest('[data-speed]');
37615
+ if (!item)
37616
+ 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');
37624
+ });
37625
+ this.qualityMenu.addEventListener('click', (e) => {
37626
+ e.stopPropagation();
37627
+ const item = e.target.closest('[data-quality]');
37628
+ if (!item)
37629
+ return;
37630
+ const idx = parseInt(item.dataset['quality']);
37631
+ this.corePlayer.setQuality(idx);
37632
+ this.currentQualityIdx = idx;
37633
+ this.refreshQualityMenu();
37634
+ this.closeMenus();
37635
+ });
37636
+ this.clickArea.addEventListener('click', (e) => {
37637
+ this.addRipple(e);
37638
+ this.togglePlay();
37639
+ });
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());
37644
+ this.root.addEventListener('mouseleave', () => {
37645
+ 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
+ }
37681
+ });
37682
+ }
37683
+ onStateChange(state) {
37684
+ this.playerState = state;
37685
+ const loading = state === exports.PlayerState.LOADING || state === exports.PlayerState.BUFFERING;
37686
+ const idle = state === exports.PlayerState.IDLE;
37687
+ const ended = this.videoEl.ended;
37688
+ this.spinner.classList.toggle('gvp-spinner-hidden', !loading);
37689
+ const showCenter = (idle || ended) && !loading;
37690
+ this.centerPlay.classList.toggle('gvp-center-play-hidden', !showCenter);
37691
+ if (showCenter) {
37692
+ this.centerPlayBtn.innerHTML = '';
37693
+ this.centerPlayBtn.appendChild(svg(ended ? ICON.replay : ICON.play, 32, 32));
37694
+ }
37695
+ this.clickArea.classList.toggle('gvp-click-area-hidden', idle);
37696
+ if (state !== exports.PlayerState.PLAYING) {
37697
+ this.showControls(true);
37698
+ if (this.hideTimer) {
37699
+ clearTimeout(this.hideTimer);
37700
+ this.hideTimer = null;
37701
+ }
37702
+ }
37703
+ else {
37704
+ this.resetHideTimer();
37705
+ }
37706
+ this.renderPlayBtn();
37707
+ }
37708
+ togglePlay() {
37709
+ const video = this.videoEl;
37710
+ if (video.paused || video.ended) {
37711
+ this.corePlayer.play().catch(() => { });
37712
+ }
37713
+ else {
37714
+ this.corePlayer.pause();
37715
+ }
37716
+ this.resetHideTimer();
37717
+ }
37718
+ renderPlayBtn() {
37719
+ const video = this.videoEl;
37720
+ const ended = video.ended;
37721
+ const playing = this.playerState === exports.PlayerState.PLAYING;
37722
+ 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;
37732
+ this.volBtn.innerHTML = '';
37733
+ this.volBtn.appendChild(svg(muted ? ICON.volMute : ICON.volHigh, 18, 18));
37734
+ this.volSlider.value = String(muted ? 0 : video.volume);
37735
+ }
37736
+ seekTo(e) {
37737
+ if (!this.duration)
37738
+ return;
37739
+ const rect = this.seekWrap.getBoundingClientRect();
37740
+ const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
37741
+ this.corePlayer.seek(pct * this.duration);
37742
+ }
37743
+ onSeekHover(e) {
37744
+ if (!this.duration)
37745
+ return;
37746
+ const rect = this.seekWrap.getBoundingClientRect();
37747
+ const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
37748
+ this.seekTooltip.textContent = formatTime(pct * this.duration);
37749
+ this.seekTooltip.style.left = `${pct * 100}%`;
37750
+ this.seekTooltip.style.opacity = '1';
37751
+ }
37752
+ onTimeUpdate() {
37753
+ const video = this.videoEl;
37754
+ const ct = video.currentTime;
37755
+ this.timeEl.textContent = `${formatTime(ct)} / ${formatTime(this.duration)}`;
37756
+ this.updateSeekBar();
37757
+ }
37758
+ updateSeekBar() {
37759
+ const ct = this.videoEl.currentTime;
37760
+ const dur = this.duration;
37761
+ if (!dur)
37762
+ return;
37763
+ const pPct = (ct / dur) * 100;
37764
+ const bPct = (this.bufferedEnd / dur) * 100;
37765
+ this.seekProgress.style.width = `${pPct}%`;
37766
+ this.seekBuffered.style.width = `${bPct}%`;
37767
+ this.seekThumb.style.left = `${pPct}%`;
37768
+ }
37769
+ buildQualityMenu() {
37770
+ if (!this.qualityLevels.length)
37771
+ return;
37772
+ this.settingsBtn.style.display = '';
37773
+ this.qualityMenu.innerHTML = '';
37774
+ const title = el('div', 'gvp-menu-title');
37775
+ title.textContent = 'Quality';
37776
+ const autoItem = el('div', 'gvp-menu-item gvp-menu-item-active');
37777
+ autoItem.dataset['quality'] = '-1';
37778
+ 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);
37784
+ [...this.qualityLevels].reverse().forEach(q => {
37785
+ const item = el('div', 'gvp-menu-item');
37786
+ item.dataset['quality'] = String(q.index);
37787
+ item.textContent = q.name;
37788
+ this.qualityMenu.appendChild(item);
37789
+ });
37790
+ this.currentQualityIdx = -1;
37791
+ }
37792
+ refreshQualityMenu() {
37793
+ 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' : ''}`;
37797
+ const existing = item.querySelector('.gvp-menu-check');
37798
+ if (active && !existing) {
37799
+ const check = el('span', 'gvp-menu-check');
37800
+ check.textContent = '✓';
37801
+ item.appendChild(check);
37802
+ }
37803
+ else if (!active && existing) {
37804
+ existing.remove();
37805
+ }
37806
+ });
37301
37807
  }
37302
- static createFromElement(videoElement, videoId, config) {
37303
- const element = typeof videoElement === 'string'
37304
- ? document.getElementById(videoElement)
37305
- : videoElement;
37306
- if (!element || element.tagName !== 'VIDEO') {
37307
- throw new Error('Invalid video element provided');
37808
+ refreshSpeedMenu() {
37809
+ 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' : ''}`;
37812
+ });
37813
+ }
37814
+ toggleMenu(which) {
37815
+ const same = this.openMenu === which;
37816
+ this.closeMenus();
37817
+ if (!same) {
37818
+ this.openMenu = which;
37819
+ if (which === 'speed') {
37820
+ this.refreshSpeedMenu();
37821
+ this.speedMenu.classList.remove('gvp-menu-hidden');
37822
+ }
37823
+ else {
37824
+ this.refreshQualityMenu();
37825
+ this.qualityMenu.classList.remove('gvp-menu-hidden');
37826
+ }
37308
37827
  }
37309
- return new GuardVideoPlayer(element, videoId, config);
37310
37828
  }
37829
+ closeMenus() {
37830
+ this.openMenu = null;
37831
+ this.speedMenu.classList.add('gvp-menu-hidden');
37832
+ this.qualityMenu.classList.add('gvp-menu-hidden');
37833
+ }
37834
+ showControls(visible) {
37835
+ this.controls.classList.toggle('gvp-controls-hidden', !visible);
37836
+ this.badge.classList.toggle('gvp-badge-hidden', !visible);
37837
+ }
37838
+ resetHideTimer() {
37839
+ this.showControls(true);
37840
+ if (this.hideTimer)
37841
+ clearTimeout(this.hideTimer);
37842
+ this.hideTimer = setTimeout(() => {
37843
+ if (this.playerState === exports.PlayerState.PLAYING)
37844
+ this.showControls(false);
37845
+ }, 2800);
37846
+ }
37847
+ toggleFullscreen() {
37848
+ if (document.fullscreenElement) {
37849
+ document.exitFullscreen().catch(() => { });
37850
+ }
37851
+ else {
37852
+ this.root.requestFullscreen().catch(() => { });
37853
+ }
37854
+ }
37855
+ onFsChange() {
37856
+ const fs = !!document.fullscreenElement;
37857
+ 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;
37874
+ this.watermarkDiv.innerHTML = '';
37875
+ for (let i = 0; i < 20; i++) {
37876
+ const span = el('span', 'gvp-watermark-text');
37877
+ span.textContent = this.watermarkText;
37878
+ span.style.left = `${(i % 4) * 26 + (Math.floor(i / 4) % 2) * 13}%`;
37879
+ span.style.top = `${Math.floor(i / 4) * 22}%`;
37880
+ this.watermarkDiv.appendChild(span);
37881
+ }
37882
+ }
37883
+ addRipple(e) {
37884
+ const rect = this.root.getBoundingClientRect();
37885
+ const d = document.createElement('div');
37886
+ 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`;
37889
+ this.root.appendChild(d);
37890
+ setTimeout(() => d.remove(), 600);
37891
+ }
37892
+ play() { return this.corePlayer.play(); }
37893
+ pause() { return this.corePlayer.pause(); }
37894
+ seek(t) { return this.corePlayer.seek(t); }
37895
+ getCurrentTime() { return this.corePlayer.getCurrentTime(); }
37896
+ getDuration() { return this.corePlayer.getDuration(); }
37897
+ getVolume() { return this.corePlayer.getVolume(); }
37898
+ setVolume(v) { return this.corePlayer.setVolume(v); }
37899
+ getQualityLevels() { return this.corePlayer.getQualityLevels(); }
37900
+ getCurrentQuality() { return this.corePlayer.getCurrentQuality(); }
37901
+ setQuality(i) { return this.corePlayer.setQuality(i); }
37902
+ getState() { return this.corePlayer.getState(); }
37903
+ getVideoElement() { return this.videoEl; }
37904
+ destroy() {
37905
+ if (this.hideTimer)
37906
+ clearTimeout(this.hideTimer);
37907
+ this.corePlayer.destroy();
37908
+ this.root.remove();
37909
+ }
37910
+ }
37911
+ class GuardVideoPlayer {
37912
+ constructor(ui) {
37913
+ this.ui = ui;
37914
+ }
37915
+ static create(containerId, videoId, config) {
37916
+ 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));
37920
+ }
37921
+ static createInElement(container, videoId, config) {
37922
+ return new GuardVideoPlayer(new VanillaPlayerUI(container, videoId, config));
37923
+ }
37924
+ play() { return this.ui.play(); }
37925
+ pause() { return this.ui.pause(); }
37926
+ seek(time) { return this.ui.seek(time); }
37927
+ getCurrentTime() { return this.ui.getCurrentTime(); }
37928
+ getDuration() { return this.ui.getDuration(); }
37929
+ getVolume() { return this.ui.getVolume(); }
37930
+ setVolume(volume) { return this.ui.setVolume(volume); }
37931
+ getQualityLevels() { return this.ui.getQualityLevels(); }
37932
+ getCurrentQuality() { return this.ui.getCurrentQuality(); }
37933
+ setQuality(idx) { return this.ui.setQuality(idx); }
37934
+ getState() { return this.ui.getState(); }
37935
+ getVideoElement() { return this.ui.getVideoElement(); }
37936
+ destroy() { return this.ui.destroy(); }
37311
37937
  }
37312
37938
  if (typeof window !== 'undefined') {
37313
37939
  window.GuardVideoPlayer = GuardVideoPlayer;