@guardvideo/player-sdk 2.0.0 → 2.1.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.
@@ -36657,23 +36657,12 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36657
36657
  let GuardVideoPlayer$1 = class GuardVideoPlayer {
36658
36658
  constructor(videoElement, videoId, config) {
36659
36659
  this.videoId = videoId;
36660
- this.container = null;
36661
36660
  this.hls = null;
36662
36661
  this.state = exports.PlayerState.IDLE;
36663
36662
  this.embedToken = null;
36664
36663
  this.currentQuality = null;
36665
- this.ctxMenu = null;
36666
- this.ctxStyleTag = null;
36667
- this.watermarkEl = null;
36668
- this.watermarkObserver = null;
36669
- this._onCtx = this.handleContextMenu.bind(this);
36670
- this._onDocClick = this.hideContextMenu.bind(this);
36671
- this._onKeyDown = this.handleKeyDown.bind(this);
36672
36664
  this._onRateChange = this.enforceMaxRate.bind(this);
36673
- this._onSelectStart = (e) => e.preventDefault();
36674
- this._onDragStart = (e) => e.preventDefault();
36675
36665
  this.videoElement = videoElement;
36676
- this.container = videoElement.parentElement;
36677
36666
  this.config = {
36678
36667
  embedTokenEndpoint: config.embedTokenEndpoint,
36679
36668
  apiBaseUrl: config.apiBaseUrl || '',
@@ -36693,6 +36682,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36693
36682
  onError: config.onError || (() => { }),
36694
36683
  onQualityChange: config.onQualityChange || (() => { }),
36695
36684
  onStateChange: config.onStateChange || (() => { }),
36685
+ onWatermark: config.onWatermark || (() => { }),
36696
36686
  };
36697
36687
  this.log('Initializing GuardVideo Player', { videoId, config });
36698
36688
  if (!this.checkAllowedDomain())
@@ -36702,17 +36692,17 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36702
36692
  }
36703
36693
  log(message, data) {
36704
36694
  if (this.config.debug) {
36705
- console.log(`[GuardVideoPlayer] ${message}`, data || '');
36695
+ console.log('[GuardVideoPlayer] ' + message, data || '');
36706
36696
  }
36707
36697
  }
36708
36698
  error(message, data) {
36709
- console.error(`[GuardVideoPlayer] ${message}`, data || '');
36699
+ console.error('[GuardVideoPlayer] ' + message, data || '');
36710
36700
  }
36711
36701
  setState(newState) {
36712
36702
  if (this.state !== newState) {
36713
36703
  this.state = newState;
36714
36704
  this.config.onStateChange(newState);
36715
- this.log(`State changed to: ${newState}`);
36705
+ this.log('State changed to: ' + newState);
36716
36706
  }
36717
36707
  }
36718
36708
  checkAllowedDomain() {
@@ -36720,11 +36710,11 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36720
36710
  if (!domains || domains.length === 0)
36721
36711
  return true;
36722
36712
  const currentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
36723
- const allowed = domains.some((d) => currentOrigin === d || currentOrigin.endsWith(`.${d.replace(/^https?:\/\//, '')}`));
36713
+ const allowed = domains.some((d) => currentOrigin === d || currentOrigin.endsWith('.' + d.replace(/^https?:\/\//, '')));
36724
36714
  if (!allowed) {
36725
36715
  this.handleError({
36726
36716
  code: 'DOMAIN_NOT_ALLOWED',
36727
- message: `This player is not authorized to run on ${currentOrigin}`,
36717
+ message: 'This player is not authorized to run on ' + currentOrigin,
36728
36718
  fatal: true,
36729
36719
  });
36730
36720
  return false;
@@ -36733,294 +36723,24 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36733
36723
  }
36734
36724
  applySecurity() {
36735
36725
  const sec = this.config.security;
36736
- const target = this.container || this.videoElement;
36737
- target.addEventListener('contextmenu', this._onCtx);
36738
- document.addEventListener('click', this._onDocClick);
36739
- if (sec.disableSelection) {
36740
- target.addEventListener('selectstart', this._onSelectStart);
36741
- target.style.userSelect = 'none';
36742
- target.style.webkitUserSelect = 'none';
36743
- }
36744
- if (sec.disableDrag) {
36745
- this.videoElement.addEventListener('dragstart', this._onDragStart);
36746
- this.videoElement.draggable = false;
36747
- }
36748
36726
  if (sec.disablePiP) {
36749
36727
  this.videoElement.disablePictureInPicture = true;
36750
36728
  }
36751
36729
  if (sec.disableScreenCapture) {
36752
- if ('mediaKeys' in this.videoElement && typeof navigator.requestMediaKeySystemAccess === 'function') {
36730
+ if ('mediaKeys' in this.videoElement &&
36731
+ typeof navigator.requestMediaKeySystemAccess === 'function') {
36753
36732
  this.log('Screen-capture protection: EME hint applied');
36754
36733
  }
36755
- target.style.setProperty('-webkit-app-region', 'no-drag');
36756
- }
36757
- if (sec.blockDevTools) {
36758
- document.addEventListener('keydown', this._onKeyDown);
36759
36734
  }
36760
36735
  if (sec.maxPlaybackRate) {
36761
36736
  this.videoElement.addEventListener('ratechange', this._onRateChange);
36762
36737
  }
36763
- if (sec.enableWatermark && sec.watermarkText && this.container) {
36764
- this.createWatermark(sec.watermarkText);
36765
- }
36766
- this.injectProtectiveStyles();
36767
- }
36768
- handleContextMenu(e) {
36769
- e.preventDefault();
36770
- e.stopPropagation();
36771
- const sec = this.config.security;
36772
- if (sec.disableRightClick)
36773
- return;
36774
- const me = e;
36775
- this.showContextMenu(me.clientX, me.clientY);
36776
- }
36777
- showContextMenu(x, y) {
36778
- this.hideContextMenu();
36779
- const branding = this.config.branding;
36780
- const extraItems = this.config.contextMenuItems;
36781
- const menu = document.createElement('div');
36782
- menu.className = 'gv-ctx-menu';
36783
- menu.setAttribute('role', 'menu');
36784
- const header = document.createElement('a');
36785
- header.className = 'gv-ctx-header';
36786
- header.href = branding.url;
36787
- header.target = '_blank';
36788
- header.rel = 'noopener noreferrer';
36789
- header.setAttribute('role', 'menuitem');
36790
- if (branding.logoUrl) {
36791
- const logo = document.createElement('img');
36792
- logo.src = branding.logoUrl;
36793
- logo.alt = branding.name;
36794
- logo.className = 'gv-ctx-logo';
36795
- logo.width = 20;
36796
- logo.height = 20;
36797
- header.appendChild(logo);
36798
- }
36799
- else {
36800
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
36801
- svg.setAttribute('width', '18');
36802
- svg.setAttribute('height', '18');
36803
- svg.setAttribute('viewBox', '0 0 24 24');
36804
- svg.setAttribute('fill', 'none');
36805
- svg.setAttribute('stroke', branding.accentColor);
36806
- svg.setAttribute('stroke-width', '2');
36807
- svg.setAttribute('stroke-linecap', 'round');
36808
- svg.setAttribute('stroke-linejoin', 'round');
36809
- svg.innerHTML = '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>';
36810
- header.appendChild(svg);
36811
- }
36812
- const nameSpan = document.createElement('span');
36813
- nameSpan.className = 'gv-ctx-brand-name';
36814
- nameSpan.textContent = branding.name;
36815
- header.appendChild(nameSpan);
36816
- const tagSpan = document.createElement('span');
36817
- tagSpan.className = 'gv-ctx-tag';
36818
- tagSpan.textContent = 'Secure Video Player';
36819
- header.appendChild(tagSpan);
36820
- menu.appendChild(header);
36821
- if (extraItems.length > 0) {
36822
- extraItems.forEach((item) => {
36823
- if (item.separator) {
36824
- const sep = document.createElement('div');
36825
- sep.className = 'gv-ctx-sep';
36826
- menu.appendChild(sep);
36827
- }
36828
- const row = document.createElement('div');
36829
- row.className = 'gv-ctx-item';
36830
- row.setAttribute('role', 'menuitem');
36831
- row.tabIndex = 0;
36832
- if (item.icon) {
36833
- const ico = document.createElement('img');
36834
- ico.src = item.icon;
36835
- ico.width = 14;
36836
- ico.height = 14;
36837
- ico.className = 'gv-ctx-item-icon';
36838
- row.appendChild(ico);
36839
- }
36840
- const label = document.createElement('span');
36841
- label.textContent = item.label;
36842
- row.appendChild(label);
36843
- row.addEventListener('click', (ev) => {
36844
- ev.stopPropagation();
36845
- this.hideContextMenu();
36846
- if (item.onClick) {
36847
- item.onClick();
36848
- }
36849
- else if (item.href) {
36850
- window.open(item.href, '_blank', 'noopener,noreferrer');
36851
- }
36852
- });
36853
- menu.appendChild(row);
36854
- });
36855
- }
36856
- const sep = document.createElement('div');
36857
- sep.className = 'gv-ctx-sep';
36858
- menu.appendChild(sep);
36859
- const version = document.createElement('div');
36860
- version.className = 'gv-ctx-version';
36861
- version.textContent = `${branding.name} Player v1.0`;
36862
- menu.appendChild(version);
36863
- document.body.appendChild(menu);
36864
- const rect = menu.getBoundingClientRect();
36865
- const vw = window.innerWidth;
36866
- const vh = window.innerHeight;
36867
- menu.style.left = `${x + rect.width > vw ? vw - rect.width - 8 : x}px`;
36868
- menu.style.top = `${y + rect.height > vh ? vh - rect.height - 8 : y}px`;
36869
- this.ctxMenu = menu;
36870
- }
36871
- hideContextMenu() {
36872
- if (this.ctxMenu) {
36873
- this.ctxMenu.remove();
36874
- this.ctxMenu = null;
36875
- }
36876
- }
36877
- injectProtectiveStyles() {
36878
- if (this.ctxStyleTag)
36879
- return;
36880
- const branding = this.config.branding;
36881
- const accent = branding.accentColor;
36882
- const css = `
36883
- /* GuardVideo branded context menu */
36884
- .gv-ctx-menu {
36885
- position: fixed;
36886
- z-index: 2147483647;
36887
- min-width: 220px;
36888
- background: rgba(18, 18, 22, 0.96);
36889
- backdrop-filter: blur(12px);
36890
- -webkit-backdrop-filter: blur(12px);
36891
- border: 1px solid rgba(255,255,255,0.08);
36892
- border-radius: 10px;
36893
- padding: 6px 0;
36894
- box-shadow: 0 8px 32px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.04);
36895
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
36896
- font-size: 13px;
36897
- color: #e4e4e7;
36898
- user-select: none;
36899
- animation: gv-ctx-in 0.12s ease-out;
36900
- }
36901
- @keyframes gv-ctx-in {
36902
- from { opacity: 0; transform: scale(0.96); }
36903
- to { opacity: 1; transform: scale(1); }
36904
- }
36905
-
36906
- .gv-ctx-header {
36907
- display: flex;
36908
- align-items: center;
36909
- gap: 8px;
36910
- padding: 8px 14px 8px 12px;
36911
- text-decoration: none;
36912
- color: inherit;
36913
- transition: background 0.15s;
36914
- border-radius: 6px 6px 0 0;
36915
- }
36916
- .gv-ctx-header:hover { background: rgba(255,255,255,0.06); }
36917
-
36918
- .gv-ctx-logo { border-radius: 4px; }
36919
-
36920
- .gv-ctx-brand-name {
36921
- font-weight: 600;
36922
- color: ${accent};
36923
- white-space: nowrap;
36924
- }
36925
-
36926
- .gv-ctx-tag {
36927
- margin-left: auto;
36928
- font-size: 10px;
36929
- color: rgba(255,255,255,0.35);
36930
- white-space: nowrap;
36931
- }
36932
-
36933
- .gv-ctx-sep {
36934
- height: 1px;
36935
- margin: 4px 10px;
36936
- background: rgba(255,255,255,0.07);
36937
- }
36938
-
36939
- .gv-ctx-item {
36940
- display: flex;
36941
- align-items: center;
36942
- gap: 8px;
36943
- padding: 7px 14px 7px 12px;
36944
- cursor: pointer;
36945
- transition: background 0.15s;
36946
- }
36947
- .gv-ctx-item:hover { background: rgba(255,255,255,0.06); }
36948
- .gv-ctx-item-icon { border-radius: 2px; }
36949
-
36950
- .gv-ctx-version {
36951
- padding: 4px 14px 6px 12px;
36952
- font-size: 10px;
36953
- color: rgba(255,255,255,0.25);
36954
- }
36955
-
36956
- /* Watermark overlay */
36957
- .gv-watermark {
36958
- position: absolute;
36959
- inset: 0;
36960
- pointer-events: none;
36961
- overflow: hidden;
36962
- z-index: 10;
36963
- }
36964
- .gv-watermark-text {
36965
- position: absolute;
36966
- white-space: nowrap;
36967
- font-size: 14px;
36968
- font-family: monospace;
36969
- color: rgba(255,255,255,0.07);
36970
- transform: rotate(-30deg);
36971
- user-select: none;
36972
- pointer-events: none;
36973
- }
36974
- `;
36975
- const tag = document.createElement('style');
36976
- tag.setAttribute('data-guardvideo', 'player-styles');
36977
- tag.textContent = css;
36978
- document.head.appendChild(tag);
36979
- this.ctxStyleTag = tag;
36980
- }
36981
- createWatermark(text) {
36982
- if (!this.container)
36983
- return;
36984
- const overlay = document.createElement('div');
36985
- overlay.className = 'gv-watermark';
36986
- for (let row = 0; row < 5; row++) {
36987
- for (let col = 0; col < 4; col++) {
36988
- const span = document.createElement('span');
36989
- span.className = 'gv-watermark-text';
36990
- span.textContent = text;
36991
- span.style.left = `${col * 28 + (row % 2) * 14}%`;
36992
- span.style.top = `${row * 22}%`;
36993
- overlay.appendChild(span);
36994
- }
36995
- }
36996
- this.container.style.position = 'relative';
36997
- this.container.appendChild(overlay);
36998
- this.watermarkEl = overlay;
36999
- this.watermarkObserver = new MutationObserver(() => {
37000
- if (this.container && this.watermarkEl && !this.container.contains(this.watermarkEl)) {
37001
- this.container.appendChild(this.watermarkEl);
37002
- }
37003
- });
37004
- this.watermarkObserver.observe(this.container, { childList: true, subtree: false });
37005
- }
37006
- handleKeyDown(e) {
37007
- if (e.key === 'F12') {
37008
- e.preventDefault();
37009
- return;
37010
- }
37011
- if (e.ctrlKey && e.shiftKey && ['I', 'J', 'C'].includes(e.key.toUpperCase())) {
37012
- e.preventDefault();
37013
- return;
37014
- }
37015
- if (e.ctrlKey && e.key.toUpperCase() === 'U') {
37016
- e.preventDefault();
37017
- }
37018
36738
  }
37019
36739
  enforceMaxRate() {
37020
36740
  const max = this.config.security.maxPlaybackRate;
37021
36741
  if (this.videoElement.playbackRate > max) {
37022
36742
  this.videoElement.playbackRate = max;
37023
- this.log(`Playback rate clamped to ${max}`);
36743
+ this.log('Playback rate clamped to ' + max);
37024
36744
  }
37025
36745
  }
37026
36746
  async initialize() {
@@ -37045,38 +36765,38 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37045
36765
  return;
37046
36766
  try {
37047
36767
  const tokenId = this.embedToken.tokenId;
37048
- const url = `${this.config.apiBaseUrl}/videos/stream/${this.videoId}/viewer-config?token=${encodeURIComponent(tokenId)}`;
36768
+ const url = this.config.apiBaseUrl +
36769
+ '/videos/stream/' + this.videoId +
36770
+ '/viewer-config?token=' + encodeURIComponent(tokenId);
37049
36771
  const resp = await fetch(url, { credentials: 'omit' });
37050
36772
  if (!resp.ok) {
37051
36773
  this.log('viewer-config fetch failed, falling back to SDK config', resp.status);
37052
36774
  const sec = this.config.security;
37053
- if (sec.enableWatermark && sec.watermarkText && this.container) {
37054
- this.createWatermark(sec.watermarkText);
36775
+ if (sec.enableWatermark && sec.watermarkText) {
36776
+ this.config.onWatermark?.(sec.watermarkText);
37055
36777
  }
37056
36778
  return;
37057
36779
  }
37058
36780
  const cfg = await resp.json();
37059
36781
  this.log('Watermark config from server:', cfg);
37060
- if (cfg.enableWatermark && cfg.watermarkText && this.container) {
37061
- this.createWatermark(cfg.watermarkText);
36782
+ if (cfg.enableWatermark && cfg.watermarkText) {
36783
+ this.config.onWatermark?.(cfg.watermarkText);
37062
36784
  }
37063
36785
  }
37064
36786
  catch (err) {
37065
36787
  this.log('fetchAndApplyWatermark error (non-fatal):', err);
37066
36788
  const sec = this.config.security;
37067
- if (sec.enableWatermark && sec.watermarkText && this.container) {
37068
- this.createWatermark(sec.watermarkText);
36789
+ if (sec.enableWatermark && sec.watermarkText) {
36790
+ this.config.onWatermark?.(sec.watermarkText);
37069
36791
  }
37070
36792
  }
37071
36793
  }
37072
36794
  async fetchEmbedToken() {
37073
- const url = `${this.config.embedTokenEndpoint}/${this.videoId}`;
36795
+ const url = this.config.embedTokenEndpoint + '/' + this.videoId;
37074
36796
  this.log('Fetching embed token from', url);
37075
36797
  const response = await fetch(url, {
37076
36798
  method: 'POST',
37077
- headers: {
37078
- 'Content-Type': 'application/json',
37079
- },
36799
+ headers: { 'Content-Type': 'application/json' },
37080
36800
  body: JSON.stringify({
37081
36801
  allowedDomain: window.location.origin,
37082
36802
  expiresInMinutes: 120,
@@ -37128,14 +36848,11 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37128
36848
  this.hls.loadSource(playerUrl);
37129
36849
  this.hls.attachMedia(this.videoElement);
37130
36850
  this.hls.on(Hls.Events.MANIFEST_PARSED, (_event, data) => {
37131
- this.log('HLS manifest parsed', {
37132
- levels: data.levels.map((l) => `${l.height}p`),
37133
- });
36851
+ this.log('HLS manifest parsed', { levels: data.levels.map((l) => l.height + 'p') });
37134
36852
  this.setState(exports.PlayerState.READY);
37135
36853
  this.config.onReady();
37136
- if (this.config.autoplay) {
36854
+ if (this.config.autoplay)
37137
36855
  this.play();
37138
- }
37139
36856
  });
37140
36857
  this.hls.on(Hls.Events.LEVEL_SWITCHED, (_event, data) => {
37141
36858
  const level = this.hls.levels[data.level];
@@ -37144,10 +36861,10 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37144
36861
  height: level.height,
37145
36862
  width: level.width,
37146
36863
  bitrate: level.bitrate,
37147
- name: `${level.height}p`,
36864
+ name: level.height + 'p',
37148
36865
  };
37149
36866
  this.currentQuality = quality;
37150
- this.log(`Quality switched to ${quality.name}`);
36867
+ this.log('Quality switched to ' + quality.name);
37151
36868
  this.config.onQualityChange(quality.name);
37152
36869
  });
37153
36870
  this.hls.on(Hls.Events.ERROR, (_event, data) => {
@@ -37163,40 +36880,28 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37163
36880
  this.hls?.recoverMediaError();
37164
36881
  break;
37165
36882
  default:
37166
- this.handleError({
37167
- code: data.type,
37168
- message: data.details,
37169
- fatal: true,
37170
- details: data,
37171
- });
37172
- break;
36883
+ this.handleError({ code: data.type, message: data.details, fatal: true, details: data });
37173
36884
  }
37174
36885
  }
37175
36886
  });
37176
36887
  this.setupVideoEventListeners();
37177
36888
  }
37178
36889
  setupVideoEventListeners() {
37179
- this.videoElement.addEventListener('playing', () => {
37180
- this.setState(exports.PlayerState.PLAYING);
37181
- });
37182
- this.videoElement.addEventListener('pause', () => {
37183
- this.setState(exports.PlayerState.PAUSED);
37184
- });
37185
- this.videoElement.addEventListener('waiting', () => {
37186
- this.setState(exports.PlayerState.BUFFERING);
37187
- });
36890
+ this.videoElement.addEventListener('playing', () => this.setState(exports.PlayerState.PLAYING));
36891
+ this.videoElement.addEventListener('pause', () => this.setState(exports.PlayerState.PAUSED));
36892
+ this.videoElement.addEventListener('waiting', () => this.setState(exports.PlayerState.BUFFERING));
37188
36893
  this.videoElement.addEventListener('error', () => {
37189
36894
  const error = this.videoElement.error;
37190
36895
  if (error) {
37191
- const errorMessages = {
36896
+ const msgs = {
37192
36897
  1: 'Video loading aborted',
37193
36898
  2: 'Network error',
37194
36899
  3: 'Video decoding failed',
37195
36900
  4: 'Video format not supported',
37196
36901
  };
37197
36902
  this.handleError({
37198
- code: `MEDIA_ERROR_${error.code}`,
37199
- message: errorMessages[error.code] || 'Unknown media error',
36903
+ code: 'MEDIA_ERROR_' + error.code,
36904
+ message: msgs[error.code] || 'Unknown media error',
37200
36905
  fatal: true,
37201
36906
  details: error,
37202
36907
  });
@@ -37217,21 +36922,11 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37217
36922
  throw err;
37218
36923
  }
37219
36924
  }
37220
- pause() {
37221
- this.videoElement.pause();
37222
- }
37223
- getCurrentTime() {
37224
- return this.videoElement.currentTime;
37225
- }
37226
- seek(time) {
37227
- this.videoElement.currentTime = time;
37228
- }
37229
- getDuration() {
37230
- return this.videoElement.duration || 0;
37231
- }
37232
- getVolume() {
37233
- return this.videoElement.volume;
37234
- }
36925
+ pause() { this.videoElement.pause(); }
36926
+ getCurrentTime() { return this.videoElement.currentTime; }
36927
+ seek(time) { this.videoElement.currentTime = time; }
36928
+ getDuration() { return this.videoElement.duration || 0; }
36929
+ getVolume() { return this.videoElement.volume; }
37235
36930
  setVolume(volume) {
37236
36931
  this.videoElement.volume = Math.max(0, Math.min(1, volume));
37237
36932
  }
@@ -37243,34 +36938,20 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37243
36938
  height: level.height,
37244
36939
  width: level.width,
37245
36940
  bitrate: level.bitrate,
37246
- name: `${level.height}p`,
36941
+ name: level.height + 'p',
37247
36942
  }));
37248
36943
  }
37249
- getCurrentQuality() {
37250
- return this.currentQuality;
37251
- }
36944
+ getCurrentQuality() { return this.currentQuality; }
37252
36945
  setQuality(levelIndex) {
37253
36946
  if (this.hls) {
37254
36947
  this.hls.currentLevel = levelIndex;
37255
- this.log(`Quality set to level ${levelIndex}`);
36948
+ this.log('Quality set to level ' + levelIndex);
37256
36949
  }
37257
36950
  }
37258
- getState() {
37259
- return this.state;
37260
- }
36951
+ getState() { return this.state; }
37261
36952
  destroy() {
37262
36953
  this.log('Destroying player');
37263
- const target = this.container || this.videoElement;
37264
- target.removeEventListener('contextmenu', this._onCtx);
37265
- target.removeEventListener('selectstart', this._onSelectStart);
37266
- this.videoElement.removeEventListener('dragstart', this._onDragStart);
37267
36954
  this.videoElement.removeEventListener('ratechange', this._onRateChange);
37268
- document.removeEventListener('click', this._onDocClick);
37269
- document.removeEventListener('keydown', this._onKeyDown);
37270
- this.hideContextMenu();
37271
- this.watermarkObserver?.disconnect();
37272
- this.watermarkEl?.remove();
37273
- this.ctxStyleTag?.remove();
37274
36955
  if (this.hls) {
37275
36956
  this.hls.destroy();
37276
36957
  this.hls = null;
@@ -37357,6 +37038,10 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37357
37038
  -ms-user-select: none;
37358
37039
  user-select: none;
37359
37040
  outline: none;
37041
+ /* Reserve space at the bottom so the video is never covered by the controls bar.
37042
+ Controls bar ≈ 10px top padding + seek(~20px) + btn row(~34px) + 14px bottom = ~88px.
37043
+ We add this as padding-bottom so the video shrinks up rather than sitting behind the bar. */
37044
+ padding-bottom: 90px;
37360
37045
 
37361
37046
  /* Subtle inner vignette for cinema depth */
37362
37047
  box-shadow:
@@ -37670,6 +37355,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37670
37355
  width: 0;
37671
37356
  max-width: 72px;
37672
37357
  height: 3px;
37358
+ /* Default background — overridden by JS inline style for the fill gradient */
37673
37359
  background: rgba(255,255,255,0.18);
37674
37360
  border-radius: 99px;
37675
37361
  outline: none;
@@ -37687,6 +37373,12 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37687
37373
  width: 72px;
37688
37374
  opacity: 1;
37689
37375
  }
37376
+ /* WebKit runnable track — gradient is set via inline style by JS */
37377
+ .gvp-volume-slider::-webkit-slider-runnable-track {
37378
+ height: 3px;
37379
+ border-radius: 99px;
37380
+ background: inherit; /* picks up the JS inline style gradient */
37381
+ }
37690
37382
  /* WebKit thumb */
37691
37383
  .gvp-volume-slider::-webkit-slider-thumb {
37692
37384
  -webkit-appearance: none;
@@ -37694,9 +37386,16 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37694
37386
  border-radius: 50%;
37695
37387
  background: #fff;
37696
37388
  cursor: pointer;
37389
+ margin-top: -4.5px; /* vertically centre over the 3px track */
37697
37390
  -webkit-box-shadow: 0 1px 4px rgba(0,0,0,0.4);
37698
37391
  box-shadow: 0 1px 4px rgba(0,0,0,0.4);
37699
37392
  }
37393
+ /* Firefox — native filled track */
37394
+ .gvp-volume-slider::-moz-range-progress {
37395
+ background: var(--gvp-accent);
37396
+ border-radius: 99px;
37397
+ height: 3px;
37398
+ }
37700
37399
  /* Firefox thumb */
37701
37400
  .gvp-volume-slider::-moz-range-thumb {
37702
37401
  width: 12px; height: 12px;
@@ -37970,6 +37669,81 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37970
37669
  .gvp-time { font-size: 11px; }
37971
37670
  .gvp-controls-inner { padding: 8px 10px; }
37972
37671
  }
37672
+
37673
+ /* ── Branded context menu ─────────────────────────────────────── */
37674
+ .gvp-ctx-menu {
37675
+ position: fixed;
37676
+ z-index: 2147483647;
37677
+ min-width: 220px;
37678
+ background: rgba(12,12,18,0.96);
37679
+ -webkit-backdrop-filter: blur(16px) saturate(180%);
37680
+ backdrop-filter: blur(16px) saturate(180%);
37681
+ border: 1px solid rgba(255,255,255,0.08);
37682
+ border-radius: 12px;
37683
+ padding: 6px 0;
37684
+ -webkit-box-shadow: 0 12px 40px rgba(0,0,0,0.55), 0 0 0 0.5px rgba(255,255,255,0.04);
37685
+ box-shadow: 0 12px 40px rgba(0,0,0,0.55), 0 0 0 0.5px rgba(255,255,255,0.04);
37686
+ font-family: var(--gvp-font, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
37687
+ font-size: 13px;
37688
+ color: rgba(255,255,255,0.9);
37689
+ -webkit-user-select: none;
37690
+ -moz-user-select: none;
37691
+ -ms-user-select: none;
37692
+ user-select: none;
37693
+ -webkit-animation: gvp-ctx-in 0.12s cubic-bezier(0.22,1,0.36,1);
37694
+ animation: gvp-ctx-in 0.12s cubic-bezier(0.22,1,0.36,1);
37695
+ }
37696
+ @-webkit-keyframes gvp-ctx-in {
37697
+ from { opacity: 0; -webkit-transform: scale(0.95); transform: scale(0.95); }
37698
+ to { opacity: 1; -webkit-transform: none; transform: none; }
37699
+ }
37700
+ @keyframes gvp-ctx-in {
37701
+ from { opacity: 0; transform: scale(0.95); }
37702
+ to { opacity: 1; transform: none; }
37703
+ }
37704
+ .gvp-ctx-header {
37705
+ display: -webkit-box; display: -ms-flexbox; display: flex;
37706
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
37707
+ gap: 8px;
37708
+ padding: 8px 14px 8px 12px;
37709
+ text-decoration: none;
37710
+ color: inherit;
37711
+ -webkit-transition: background 0.15s; transition: background 0.15s;
37712
+ border-radius: 8px 8px 0 0;
37713
+ }
37714
+ .gvp-ctx-header:hover { background: rgba(255,255,255,0.06); }
37715
+ .gvp-ctx-logo { border-radius: 4px; }
37716
+ .gvp-ctx-brand-name {
37717
+ font-weight: 600;
37718
+ color: var(--gvp-accent);
37719
+ white-space: nowrap;
37720
+ }
37721
+ .gvp-ctx-tag {
37722
+ margin-left: auto;
37723
+ font-size: 10px;
37724
+ color: rgba(255,255,255,0.3);
37725
+ white-space: nowrap;
37726
+ }
37727
+ .gvp-ctx-sep {
37728
+ height: 1px;
37729
+ margin: 4px 10px;
37730
+ background: rgba(255,255,255,0.07);
37731
+ }
37732
+ .gvp-ctx-item {
37733
+ display: -webkit-box; display: -ms-flexbox; display: flex;
37734
+ -webkit-box-align: center; -ms-flex-align: center; align-items: center;
37735
+ gap: 8px;
37736
+ padding: 7px 14px 7px 12px;
37737
+ cursor: pointer;
37738
+ -webkit-transition: background 0.15s; transition: background 0.15s;
37739
+ }
37740
+ .gvp-ctx-item:hover { background: rgba(255,255,255,0.06); }
37741
+ .gvp-ctx-item-icon { border-radius: 2px; }
37742
+ .gvp-ctx-version {
37743
+ padding: 4px 14px 6px 12px;
37744
+ font-size: 10px;
37745
+ color: rgba(255,255,255,0.25);
37746
+ }
37973
37747
  `;
37974
37748
  const tag = document.createElement('style');
37975
37749
  tag.setAttribute('data-guardvideo', 'player-ui-styles-v2');
@@ -37987,6 +37761,12 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37987
37761
  this.openMenu = null;
37988
37762
  this.hideTimer = null;
37989
37763
  this.seekDragging = false;
37764
+ this._ctxMenu = null;
37765
+ this._ctxDocClickBound = () => { };
37766
+ this._ctxTargetBound = () => { };
37767
+ this._ctxKeyDownBound = () => { };
37768
+ this._watermarkObserver = null;
37769
+ this._watermarkText = '';
37990
37770
  const accent = config.branding?.accentColor ?? '#00e5a0';
37991
37771
  const brandName = config.branding?.name ?? 'GuardVideo';
37992
37772
  injectStyles();
@@ -38134,11 +37914,13 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
38134
37914
  };
38135
37915
  this._seekTouchEndBound = () => this._endSeekDrag();
38136
37916
  this._wireEvents(videoId, config);
37917
+ requestAnimationFrame(() => this._updateVolSliderFill(1));
38137
37918
  if (config.forensicWatermark !== false) {
38138
37919
  const wmText = config.viewerEmail || config.viewerName || '';
38139
37920
  if (wmText)
38140
37921
  this._renderWatermark(wmText);
38141
37922
  }
37923
+ this._applySecurity(config);
38142
37924
  }
38143
37925
  _hexToRgba(hex, alpha) {
38144
37926
  const clean = hex.replace('#', '');
@@ -38187,6 +37969,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
38187
37969
  this._onStateChange(state);
38188
37970
  config.onStateChange?.(state);
38189
37971
  },
37972
+ onWatermark: (text) => this._renderWatermark(text),
38190
37973
  });
38191
37974
  video.addEventListener('timeupdate', () => {
38192
37975
  config.onTimeUpdate?.(video.currentTime);
@@ -38222,6 +38005,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
38222
38005
  this.volSlider.addEventListener('input', () => {
38223
38006
  video.volume = parseFloat(this.volSlider.value);
38224
38007
  video.muted = video.volume === 0;
38008
+ this._updateVolSliderFill(video.volume);
38225
38009
  });
38226
38010
  this.seekWrap.addEventListener('mousedown', (e) => {
38227
38011
  e.preventDefault();
@@ -38339,6 +38123,14 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
38339
38123
  this.playBtn.title = `${label} (k)`;
38340
38124
  }
38341
38125
  _toggleMute() { this.videoEl.muted = !this.videoEl.muted; }
38126
+ _updateVolSliderFill(vol) {
38127
+ const accent = getComputedStyle(this.root).getPropertyValue('--gvp-accent').trim() || '#00e5a0';
38128
+ const pct = Math.round(vol * 100);
38129
+ this.volSlider.style.background =
38130
+ `linear-gradient(to right,` +
38131
+ ` ${accent} 0%, ${accent} ${pct}%,` +
38132
+ ` rgba(255,255,255,0.18) ${pct}%, rgba(255,255,255,0.18) 100%)`;
38133
+ }
38342
38134
  _onVolumeChange() {
38343
38135
  const v = this.videoEl;
38344
38136
  const vol = v.muted ? 0 : v.volume;
@@ -38349,6 +38141,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
38349
38141
  this.volBtn.setAttribute('aria-label', muted ? 'Unmute' : 'Mute');
38350
38142
  this.volBtn.title = muted ? 'Unmute (m)' : 'Mute (m)';
38351
38143
  this.volSlider.value = String(vol);
38144
+ this._updateVolSliderFill(vol);
38352
38145
  }
38353
38146
  _startSeekDrag() {
38354
38147
  this.seekDragging = true;
@@ -38535,6 +38328,9 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
38535
38328
  this.controls.classList.add('gvp-hidden');
38536
38329
  }
38537
38330
  _renderWatermark(text) {
38331
+ if (!text)
38332
+ return;
38333
+ this._watermarkText = text;
38538
38334
  this.watermarkDiv.innerHTML = '';
38539
38335
  for (let i = 0; i < 20; i++) {
38540
38336
  const span = el('span', 'gvp-watermark-text');
@@ -38543,6 +38339,40 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
38543
38339
  span.style.top = `${Math.floor(i / 4) * 22}%`;
38544
38340
  this.watermarkDiv.appendChild(span);
38545
38341
  }
38342
+ this._mountWatermarkObserver();
38343
+ }
38344
+ _mountWatermarkObserver() {
38345
+ this._watermarkObserver?.disconnect();
38346
+ this._watermarkObserver = new MutationObserver((mutations) => {
38347
+ for (const m of mutations) {
38348
+ if (m.type === 'childList' && m.target === this.root) {
38349
+ if (!this.root.contains(this.watermarkDiv)) {
38350
+ this.root.appendChild(this.watermarkDiv);
38351
+ }
38352
+ }
38353
+ if (m.type === 'attributes' && m.target === this.watermarkDiv) {
38354
+ this.watermarkDiv.removeAttribute('style');
38355
+ this.watermarkDiv.className = 'gvp-watermark';
38356
+ this.watermarkDiv.setAttribute('aria-hidden', 'true');
38357
+ }
38358
+ if (m.type === 'childList' && m.target === this.watermarkDiv) {
38359
+ if (this.watermarkDiv.childElementCount < 20) {
38360
+ this._refillWatermark();
38361
+ }
38362
+ }
38363
+ }
38364
+ });
38365
+ this._watermarkObserver.observe(this.root, { childList: true });
38366
+ this._watermarkObserver.observe(this.watermarkDiv, { attributes: true, childList: true });
38367
+ }
38368
+ _refillWatermark() {
38369
+ for (let i = this.watermarkDiv.childElementCount; i < 20; i++) {
38370
+ const span = el('span', 'gvp-watermark-text');
38371
+ span.textContent = this._watermarkText;
38372
+ span.style.left = `${(i % 4) * 26 + (Math.floor(i / 4) % 2) * 13}%`;
38373
+ span.style.top = `${Math.floor(i / 4) * 22}%`;
38374
+ this.watermarkDiv.appendChild(span);
38375
+ }
38546
38376
  }
38547
38377
  _addRipple(e) {
38548
38378
  const rect = this.root.getBoundingClientRect();
@@ -38590,6 +38420,120 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
38590
38420
  break;
38591
38421
  }
38592
38422
  }
38423
+ _applySecurity(config) {
38424
+ const sec = config.security;
38425
+ this.root.style.userSelect = 'none';
38426
+ this.root.style.webkitUserSelect = 'none';
38427
+ this.root.style.msUserSelect = 'none';
38428
+ if (sec?.disableDrag !== false) {
38429
+ this.videoEl.draggable = false;
38430
+ this.videoEl.addEventListener('dragstart', (e) => e.preventDefault());
38431
+ }
38432
+ if (sec?.blockDevTools) {
38433
+ this._ctxKeyDownBound = (e) => {
38434
+ if (e.key === 'F12') {
38435
+ e.preventDefault();
38436
+ return;
38437
+ }
38438
+ if (e.ctrlKey && e.shiftKey && ['I', 'J', 'C'].includes(e.key.toUpperCase())) {
38439
+ e.preventDefault();
38440
+ return;
38441
+ }
38442
+ if (e.ctrlKey && e.key.toUpperCase() === 'U')
38443
+ e.preventDefault();
38444
+ };
38445
+ document.addEventListener('keydown', this._ctxKeyDownBound);
38446
+ }
38447
+ this._ctxDocClickBound = () => this._hideContextMenu();
38448
+ document.addEventListener('click', this._ctxDocClickBound);
38449
+ this._ctxTargetBound = (e) => {
38450
+ e.preventDefault();
38451
+ e.stopPropagation();
38452
+ if (sec?.disableRightClick)
38453
+ return;
38454
+ const me = e;
38455
+ this._showContextMenu(me.clientX, me.clientY, config);
38456
+ };
38457
+ this.root.addEventListener('contextmenu', this._ctxTargetBound);
38458
+ }
38459
+ _showContextMenu(x, y, config) {
38460
+ this._hideContextMenu();
38461
+ const br = config.branding;
38462
+ const name = br?.name ?? 'GuardVideo';
38463
+ const url = br?.url ?? 'https://guardvid.com';
38464
+ const logoUrl = br?.logoUrl ?? '';
38465
+ const accent = br?.accentColor ?? '#00e5a0';
38466
+ const extras = config.contextMenuItems ?? [];
38467
+ const menu = el('div', 'gvp-ctx-menu');
38468
+ menu.setAttribute('role', 'menu');
38469
+ menu.style.setProperty('--gvp-accent', accent);
38470
+ const header = document.createElement('a');
38471
+ header.className = 'gvp-ctx-header';
38472
+ header.href = url;
38473
+ header.target = '_blank';
38474
+ header.rel = 'noopener noreferrer';
38475
+ header.setAttribute('role', 'menuitem');
38476
+ if (logoUrl) {
38477
+ const logo = el('img', 'gvp-ctx-logo');
38478
+ logo.src = logoUrl;
38479
+ logo.alt = name;
38480
+ logo.width = 20;
38481
+ logo.height = 20;
38482
+ header.appendChild(logo);
38483
+ }
38484
+ else {
38485
+ header.appendChild(svgEl(ICON.shield, 18, 18));
38486
+ }
38487
+ const nameSpan = el('span', 'gvp-ctx-brand-name');
38488
+ nameSpan.textContent = name;
38489
+ const tagSpan = el('span', 'gvp-ctx-tag');
38490
+ tagSpan.textContent = 'Secure Video Player';
38491
+ header.append(nameSpan, tagSpan);
38492
+ menu.appendChild(header);
38493
+ extras.forEach((item) => {
38494
+ if (item.separator) {
38495
+ menu.appendChild(el('div', 'gvp-ctx-sep'));
38496
+ return;
38497
+ }
38498
+ const row = el('div', 'gvp-ctx-item');
38499
+ row.setAttribute('role', 'menuitem');
38500
+ row.setAttribute('tabindex', '0');
38501
+ if (item.icon) {
38502
+ const ico = el('img', 'gvp-ctx-item-icon');
38503
+ ico.src = item.icon;
38504
+ ico.width = 14;
38505
+ ico.height = 14;
38506
+ row.appendChild(ico);
38507
+ }
38508
+ const lbl = el('span');
38509
+ lbl.textContent = item.label;
38510
+ row.appendChild(lbl);
38511
+ row.addEventListener('click', (ev) => {
38512
+ ev.stopPropagation();
38513
+ this._hideContextMenu();
38514
+ if (item.onClick)
38515
+ item.onClick();
38516
+ else if (item.href)
38517
+ window.open(item.href, '_blank', 'noopener,noreferrer');
38518
+ });
38519
+ menu.appendChild(row);
38520
+ });
38521
+ menu.appendChild(el('div', 'gvp-ctx-sep'));
38522
+ const ver = el('div', 'gvp-ctx-version');
38523
+ ver.textContent = `${name} Player v1.0`;
38524
+ menu.appendChild(ver);
38525
+ document.body.appendChild(menu);
38526
+ const rect = menu.getBoundingClientRect();
38527
+ menu.style.left = `${x + rect.width > window.innerWidth ? window.innerWidth - rect.width - 8 : x}px`;
38528
+ menu.style.top = `${y + rect.height > window.innerHeight ? window.innerHeight - rect.height - 8 : y}px`;
38529
+ this._ctxMenu = menu;
38530
+ }
38531
+ _hideContextMenu() {
38532
+ if (this._ctxMenu) {
38533
+ this._ctxMenu.remove();
38534
+ this._ctxMenu = null;
38535
+ }
38536
+ }
38593
38537
  play() { return this.corePlayer.play(); }
38594
38538
  pause() { return this.corePlayer.pause(); }
38595
38539
  seek(t) { return this.corePlayer.seek(t); }
@@ -38613,6 +38557,10 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
38613
38557
  window.removeEventListener('mouseup', this._seekMouseUpBound);
38614
38558
  window.removeEventListener('touchmove', this._seekTouchMoveBound);
38615
38559
  window.removeEventListener('touchend', this._seekTouchEndBound);
38560
+ document.removeEventListener('click', this._ctxDocClickBound);
38561
+ document.removeEventListener('keydown', this._ctxKeyDownBound);
38562
+ this._hideContextMenu();
38563
+ this._watermarkObserver?.disconnect();
38616
38564
  this.corePlayer.destroy();
38617
38565
  this.root.remove();
38618
38566
  }