@guardvideo/player-sdk 1.0.0 → 1.0.2

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.
@@ -36636,14 +36636,44 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36636
36636
  PlayerState["ERROR"] = "error";
36637
36637
  })(exports.PlayerState || (exports.PlayerState = {}));
36638
36638
 
36639
+ const DEFAULT_BRANDING = {
36640
+ name: 'GuardVideo',
36641
+ url: 'https://guardvid.com',
36642
+ logoUrl: '',
36643
+ accentColor: '#44c09b',
36644
+ };
36645
+ const DEFAULT_SECURITY = {
36646
+ disableRightClick: false,
36647
+ disableSelection: true,
36648
+ disableDrag: true,
36649
+ enableWatermark: false,
36650
+ watermarkText: '',
36651
+ disablePiP: false,
36652
+ disableScreenCapture: false,
36653
+ blockDevTools: false,
36654
+ maxPlaybackRate: 2,
36655
+ allowedDomains: [],
36656
+ };
36639
36657
  let GuardVideoPlayer$1 = class GuardVideoPlayer {
36640
36658
  constructor(videoElement, videoId, config) {
36641
36659
  this.videoId = videoId;
36660
+ this.container = null;
36642
36661
  this.hls = null;
36643
36662
  this.state = exports.PlayerState.IDLE;
36644
36663
  this.embedToken = null;
36645
36664
  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
+ this._onRateChange = this.enforceMaxRate.bind(this);
36673
+ this._onSelectStart = (e) => e.preventDefault();
36674
+ this._onDragStart = (e) => e.preventDefault();
36646
36675
  this.videoElement = videoElement;
36676
+ this.container = videoElement.parentElement;
36647
36677
  this.config = {
36648
36678
  embedTokenEndpoint: config.embedTokenEndpoint,
36649
36679
  apiBaseUrl: config.apiBaseUrl || '',
@@ -36653,12 +36683,21 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36653
36683
  className: config.className || '',
36654
36684
  style: config.style || {},
36655
36685
  hlsConfig: config.hlsConfig || {},
36686
+ branding: { ...DEFAULT_BRANDING, ...config.branding },
36687
+ contextMenuItems: config.contextMenuItems || [],
36688
+ security: { ...DEFAULT_SECURITY, ...config.security },
36689
+ viewerName: config.viewerName || '',
36690
+ viewerEmail: config.viewerEmail || '',
36691
+ forensicWatermark: config.forensicWatermark || false,
36656
36692
  onReady: config.onReady || (() => { }),
36657
36693
  onError: config.onError || (() => { }),
36658
36694
  onQualityChange: config.onQualityChange || (() => { }),
36659
36695
  onStateChange: config.onStateChange || (() => { }),
36660
36696
  };
36661
36697
  this.log('Initializing GuardVideo Player', { videoId, config });
36698
+ if (!this.checkAllowedDomain())
36699
+ return;
36700
+ this.applySecurity();
36662
36701
  this.initialize();
36663
36702
  }
36664
36703
  log(message, data) {
@@ -36676,12 +36715,321 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36676
36715
  this.log(`State changed to: ${newState}`);
36677
36716
  }
36678
36717
  }
36718
+ checkAllowedDomain() {
36719
+ const domains = this.config.security.allowedDomains;
36720
+ if (!domains || domains.length === 0)
36721
+ return true;
36722
+ const currentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
36723
+ const allowed = domains.some((d) => currentOrigin === d || currentOrigin.endsWith(`.${d.replace(/^https?:\/\//, '')}`));
36724
+ if (!allowed) {
36725
+ this.handleError({
36726
+ code: 'DOMAIN_NOT_ALLOWED',
36727
+ message: `This player is not authorized to run on ${currentOrigin}`,
36728
+ fatal: true,
36729
+ });
36730
+ return false;
36731
+ }
36732
+ return true;
36733
+ }
36734
+ applySecurity() {
36735
+ 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
+ if (sec.disablePiP) {
36749
+ this.videoElement.disablePictureInPicture = true;
36750
+ }
36751
+ if (sec.disableScreenCapture) {
36752
+ if ('mediaKeys' in this.videoElement && typeof navigator.requestMediaKeySystemAccess === 'function') {
36753
+ this.log('Screen-capture protection: EME hint applied');
36754
+ }
36755
+ target.style.setProperty('-webkit-app-region', 'no-drag');
36756
+ }
36757
+ if (sec.blockDevTools) {
36758
+ document.addEventListener('keydown', this._onKeyDown);
36759
+ }
36760
+ if (sec.maxPlaybackRate) {
36761
+ this.videoElement.addEventListener('ratechange', this._onRateChange);
36762
+ }
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
+ }
37019
+ enforceMaxRate() {
37020
+ const max = this.config.security.maxPlaybackRate;
37021
+ if (this.videoElement.playbackRate > max) {
37022
+ this.videoElement.playbackRate = max;
37023
+ this.log(`Playback rate clamped to ${max}`);
37024
+ }
37025
+ }
36679
37026
  async initialize() {
36680
37027
  try {
36681
37028
  this.setState(exports.PlayerState.LOADING);
36682
37029
  this.embedToken = await this.fetchEmbedToken();
36683
37030
  this.log('Embed token received', this.embedToken);
36684
37031
  await this.initializePlayer();
37032
+ await this.fetchAndApplyWatermark();
36685
37033
  }
36686
37034
  catch (err) {
36687
37035
  this.handleError({
@@ -36692,6 +37040,35 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36692
37040
  });
36693
37041
  }
36694
37042
  }
37043
+ async fetchAndApplyWatermark() {
37044
+ if (!this.embedToken || !this.config.apiBaseUrl)
37045
+ return;
37046
+ try {
37047
+ const tokenId = this.embedToken.tokenId;
37048
+ const url = `${this.config.apiBaseUrl}/videos/stream/${this.videoId}/viewer-config?token=${encodeURIComponent(tokenId)}`;
37049
+ const resp = await fetch(url, { credentials: 'omit' });
37050
+ if (!resp.ok) {
37051
+ this.log('viewer-config fetch failed, falling back to SDK config', resp.status);
37052
+ const sec = this.config.security;
37053
+ if (sec.enableWatermark && sec.watermarkText && this.container) {
37054
+ this.createWatermark(sec.watermarkText);
37055
+ }
37056
+ return;
37057
+ }
37058
+ const cfg = await resp.json();
37059
+ this.log('Watermark config from server:', cfg);
37060
+ if (cfg.enableWatermark && cfg.watermarkText && this.container) {
37061
+ this.createWatermark(cfg.watermarkText);
37062
+ }
37063
+ }
37064
+ catch (err) {
37065
+ this.log('fetchAndApplyWatermark error (non-fatal):', err);
37066
+ const sec = this.config.security;
37067
+ if (sec.enableWatermark && sec.watermarkText && this.container) {
37068
+ this.createWatermark(sec.watermarkText);
37069
+ }
37070
+ }
37071
+ }
36695
37072
  async fetchEmbedToken() {
36696
37073
  const url = `${this.config.embedTokenEndpoint}/${this.videoId}`;
36697
37074
  this.log('Fetching embed token from', url);
@@ -36704,6 +37081,9 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36704
37081
  allowedDomain: window.location.origin,
36705
37082
  expiresInMinutes: 120,
36706
37083
  maxViews: null,
37084
+ ...(this.config.viewerName ? { viewerName: this.config.viewerName } : {}),
37085
+ ...(this.config.viewerEmail ? { viewerEmail: this.config.viewerEmail } : {}),
37086
+ ...(this.config.forensicWatermark ? { forensicWatermark: this.config.forensicWatermark } : {}),
36707
37087
  }),
36708
37088
  });
36709
37089
  if (!response.ok) {
@@ -36880,6 +37260,17 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36880
37260
  }
36881
37261
  destroy() {
36882
37262
  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
+ 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();
36883
37274
  if (this.hls) {
36884
37275
  this.hls.destroy();
36885
37276
  this.hls = null;