@guardvideo/player-sdk 2.1.1 → 3.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.
@@ -36636,6 +36636,365 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36636
36636
  PlayerState["ERROR"] = "error";
36637
36637
  })(exports.PlayerState || (exports.PlayerState = {}));
36638
36638
 
36639
+ const MAX_CHUNK_SECONDS = 30;
36640
+ class WatchChunkAccumulator {
36641
+ constructor() {
36642
+ this.chunkStart = null;
36643
+ this.lastPosition = 0;
36644
+ this.pendingChunks = [];
36645
+ }
36646
+ startChunk(positionSeconds) {
36647
+ if (this.chunkStart === null) {
36648
+ this.chunkStart = positionSeconds;
36649
+ this.lastPosition = positionSeconds;
36650
+ }
36651
+ }
36652
+ tick(positionSeconds) {
36653
+ if (this.chunkStart === null)
36654
+ return;
36655
+ this.lastPosition = positionSeconds;
36656
+ const elapsed = positionSeconds - this.chunkStart;
36657
+ if (elapsed >= MAX_CHUNK_SECONDS) {
36658
+ this.finalizeChunk();
36659
+ this.chunkStart = positionSeconds;
36660
+ }
36661
+ }
36662
+ finalizeChunk() {
36663
+ if (this.chunkStart === null)
36664
+ return null;
36665
+ const duration = this.lastPosition - this.chunkStart;
36666
+ if (duration < 0.5) {
36667
+ this.chunkStart = null;
36668
+ return null;
36669
+ }
36670
+ const chunk = {
36671
+ event_type: 'watch_chunk',
36672
+ event_at: new Date().toISOString(),
36673
+ position_seconds: this.lastPosition,
36674
+ payload: {
36675
+ start_seconds: this.chunkStart,
36676
+ duration_seconds: Math.round(duration * 100) / 100,
36677
+ },
36678
+ };
36679
+ this.pendingChunks.push(chunk);
36680
+ this.chunkStart = null;
36681
+ return chunk;
36682
+ }
36683
+ drain() {
36684
+ const chunks = [...this.pendingChunks];
36685
+ this.pendingChunks = [];
36686
+ return chunks;
36687
+ }
36688
+ reset() {
36689
+ this.chunkStart = null;
36690
+ this.lastPosition = 0;
36691
+ this.pendingChunks = [];
36692
+ }
36693
+ }
36694
+
36695
+ const MAX_BATCH_SIZE = 25;
36696
+ const DEFAULT_FLUSH_INTERVAL = 5000;
36697
+ class EventTracker {
36698
+ constructor(config) {
36699
+ this.buffer = [];
36700
+ this.flushTimer = null;
36701
+ this.accumulator = new WatchChunkAccumulator();
36702
+ this.destroyed = false;
36703
+ this._onBeforeUnload = null;
36704
+ this.config = {
36705
+ endpoint: config.endpoint,
36706
+ tokenId: config.tokenId,
36707
+ sessionId: config.sessionId || '',
36708
+ eventsSecret: config.eventsSecret,
36709
+ identityHash: config.identityHash || '',
36710
+ flushIntervalMs: config.flushIntervalMs || DEFAULT_FLUSH_INTERVAL,
36711
+ debug: config.debug || false,
36712
+ };
36713
+ this.startAutoFlush();
36714
+ this.hookPageUnload();
36715
+ }
36716
+ track(type, positionSeconds, payload) {
36717
+ if (this.destroyed)
36718
+ return;
36719
+ this.buffer.push({
36720
+ event_type: type,
36721
+ event_at: new Date().toISOString(),
36722
+ position_seconds: positionSeconds,
36723
+ payload,
36724
+ });
36725
+ if (this.buffer.length >= MAX_BATCH_SIZE) {
36726
+ this.flush();
36727
+ }
36728
+ }
36729
+ startChunk(position) {
36730
+ this.accumulator.startChunk(position);
36731
+ }
36732
+ tickChunk(position) {
36733
+ this.accumulator.tick(position);
36734
+ }
36735
+ endChunk() {
36736
+ this.accumulator.finalizeChunk();
36737
+ }
36738
+ setSessionId(sessionId) {
36739
+ this.config.sessionId = sessionId;
36740
+ }
36741
+ async flush() {
36742
+ if (this.destroyed)
36743
+ return;
36744
+ const chunks = this.accumulator.drain();
36745
+ for (const c of chunks) {
36746
+ this.buffer.push(c);
36747
+ }
36748
+ if (this.buffer.length === 0)
36749
+ return;
36750
+ const batch = this.buffer.splice(0, MAX_BATCH_SIZE);
36751
+ await this.sendBatch(batch);
36752
+ }
36753
+ destroy() {
36754
+ this.destroyed = true;
36755
+ this.accumulator.finalizeChunk();
36756
+ this.flush();
36757
+ if (this.flushTimer)
36758
+ clearInterval(this.flushTimer);
36759
+ if (this._onBeforeUnload) {
36760
+ window.removeEventListener('beforeunload', this._onBeforeUnload);
36761
+ }
36762
+ }
36763
+ startAutoFlush() {
36764
+ this.flushTimer = setInterval(() => this.flush(), this.config.flushIntervalMs);
36765
+ }
36766
+ hookPageUnload() {
36767
+ if (typeof window === 'undefined')
36768
+ return;
36769
+ this._onBeforeUnload = () => {
36770
+ this.accumulator.finalizeChunk();
36771
+ const chunks = this.accumulator.drain();
36772
+ const allEvents = [...this.buffer, ...chunks];
36773
+ if (allEvents.length === 0)
36774
+ return;
36775
+ const body = JSON.stringify({
36776
+ tokenId: this.config.tokenId,
36777
+ sessionId: this.config.sessionId || undefined,
36778
+ identityHash: this.config.identityHash || undefined,
36779
+ events: allEvents.slice(0, MAX_BATCH_SIZE),
36780
+ });
36781
+ try {
36782
+ navigator.sendBeacon(this.config.endpoint, new Blob([body], { type: 'application/json' }));
36783
+ }
36784
+ catch {
36785
+ }
36786
+ };
36787
+ window.addEventListener('beforeunload', this._onBeforeUnload);
36788
+ }
36789
+ async sendBatch(events) {
36790
+ const body = JSON.stringify({
36791
+ tokenId: this.config.tokenId,
36792
+ sessionId: this.config.sessionId || undefined,
36793
+ identityHash: this.config.identityHash || undefined,
36794
+ events,
36795
+ });
36796
+ const nonce = this.randomNonce();
36797
+ const timestamp = String(Date.now());
36798
+ const signature = await this.sign(nonce, timestamp, body);
36799
+ try {
36800
+ const resp = await fetch(this.config.endpoint, {
36801
+ method: 'POST',
36802
+ headers: {
36803
+ 'Content-Type': 'application/json',
36804
+ 'X-GV-Nonce': nonce,
36805
+ 'X-GV-Timestamp': timestamp,
36806
+ 'X-GV-Signature': signature,
36807
+ },
36808
+ body,
36809
+ credentials: 'omit',
36810
+ });
36811
+ if (!resp.ok && this.config.debug) {
36812
+ console.warn('[GuardVideo EventTracker] Flush failed:', resp.status);
36813
+ }
36814
+ }
36815
+ catch (err) {
36816
+ if (this.config.debug) {
36817
+ console.warn('[GuardVideo EventTracker] Flush error:', err);
36818
+ }
36819
+ this.buffer.unshift(...events);
36820
+ }
36821
+ }
36822
+ randomNonce() {
36823
+ const arr = new Uint8Array(8);
36824
+ crypto.getRandomValues(arr);
36825
+ return Array.from(arr)
36826
+ .map((b) => b.toString(16).padStart(2, '0'))
36827
+ .join('');
36828
+ }
36829
+ async sign(nonce, timestamp, body) {
36830
+ const enc = new TextEncoder();
36831
+ const key = await crypto.subtle.importKey('raw', enc.encode(this.config.eventsSecret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
36832
+ const message = `${nonce}.${timestamp}.${body}`;
36833
+ const sig = await crypto.subtle.sign('HMAC', key, enc.encode(message));
36834
+ return Array.from(new Uint8Array(sig))
36835
+ .map((b) => b.toString(16).padStart(2, '0'))
36836
+ .join('');
36837
+ }
36838
+ }
36839
+
36840
+ async function shortHash(input) {
36841
+ const buf = new TextEncoder().encode(input);
36842
+ const digest = await crypto.subtle.digest('SHA-256', buf);
36843
+ return Array.from(new Uint8Array(digest))
36844
+ .map((b) => b.toString(16).padStart(2, '0'))
36845
+ .join('')
36846
+ .substring(0, 16);
36847
+ }
36848
+ async function longHash(input) {
36849
+ const buf = new TextEncoder().encode(input);
36850
+ const digest = await crypto.subtle.digest('SHA-256', buf);
36851
+ return Array.from(new Uint8Array(digest))
36852
+ .map((b) => b.toString(16).padStart(2, '0'))
36853
+ .join('')
36854
+ .substring(0, 32);
36855
+ }
36856
+ function collectCanvas() {
36857
+ try {
36858
+ const c = document.createElement('canvas');
36859
+ c.width = 200;
36860
+ c.height = 50;
36861
+ const ctx = c.getContext('2d');
36862
+ if (!ctx)
36863
+ return 'no-canvas';
36864
+ ctx.textBaseline = 'top';
36865
+ ctx.font = '14px Arial';
36866
+ ctx.fillStyle = '#f60';
36867
+ ctx.fillRect(125, 1, 62, 20);
36868
+ ctx.fillStyle = '#069';
36869
+ ctx.fillText('GuardVideo<canvas>', 2, 15);
36870
+ ctx.fillStyle = 'rgba(102,204,0,0.7)';
36871
+ ctx.fillText('fingerprint', 4, 35);
36872
+ return c.toDataURL();
36873
+ }
36874
+ catch {
36875
+ return 'canvas-error';
36876
+ }
36877
+ }
36878
+ async function collectAudio() {
36879
+ try {
36880
+ const ctx = new (window.OfflineAudioContext ||
36881
+ window.webkitOfflineAudioContext)(1, 44100, 44100);
36882
+ const osc = ctx.createOscillator();
36883
+ osc.type = 'triangle';
36884
+ osc.frequency.setValueAtTime(10000, ctx.currentTime);
36885
+ const comp = ctx.createDynamicsCompressor();
36886
+ comp.threshold.setValueAtTime(-50, ctx.currentTime);
36887
+ comp.knee.setValueAtTime(40, ctx.currentTime);
36888
+ comp.ratio.setValueAtTime(12, ctx.currentTime);
36889
+ comp.attack.setValueAtTime(0, ctx.currentTime);
36890
+ comp.release.setValueAtTime(0.25, ctx.currentTime);
36891
+ osc.connect(comp);
36892
+ comp.connect(ctx.destination);
36893
+ osc.start(0);
36894
+ const rendered = await ctx.startRendering();
36895
+ const data = rendered.getChannelData(0);
36896
+ let sum = 0;
36897
+ for (let i = 4500; i < 5000; i++)
36898
+ sum += Math.abs(data[i]);
36899
+ return sum.toString();
36900
+ }
36901
+ catch {
36902
+ return 'audio-error';
36903
+ }
36904
+ }
36905
+ function collectNav() {
36906
+ const n = navigator;
36907
+ return [
36908
+ n.hardwareConcurrency || 0,
36909
+ n.deviceMemory || 0,
36910
+ n.maxTouchPoints || 0,
36911
+ n.platform || '',
36912
+ n.userAgentData?.platform || '',
36913
+ n.userAgentData?.mobile ? '1' : '0',
36914
+ ].join('|');
36915
+ }
36916
+ function collectFonts() {
36917
+ try {
36918
+ const baseFonts = ['monospace', 'sans-serif', 'serif'];
36919
+ const testFonts = [
36920
+ 'Arial', 'Courier New', 'Georgia', 'Times New Roman',
36921
+ 'Verdana', 'Trebuchet MS', 'Palatino', 'Impact', 'Comic Sans MS',
36922
+ ];
36923
+ const span = document.createElement('span');
36924
+ span.style.cssText = 'position:absolute;left:-9999px;font-size:72px;visibility:hidden';
36925
+ span.textContent = 'mmmmmmmmmmlli';
36926
+ document.body.appendChild(span);
36927
+ const baseSizes = {};
36928
+ for (const bf of baseFonts) {
36929
+ span.style.fontFamily = bf;
36930
+ baseSizes[bf] = span.offsetWidth;
36931
+ }
36932
+ const detected = [];
36933
+ for (const tf of testFonts) {
36934
+ for (const bf of baseFonts) {
36935
+ span.style.fontFamily = `'${tf}', ${bf}`;
36936
+ if (span.offsetWidth !== baseSizes[bf]) {
36937
+ detected.push(tf);
36938
+ break;
36939
+ }
36940
+ }
36941
+ }
36942
+ document.body.removeChild(span);
36943
+ return detected.join(',');
36944
+ }
36945
+ catch {
36946
+ return 'font-error';
36947
+ }
36948
+ }
36949
+ function collectMedia() {
36950
+ const v = document.createElement('video');
36951
+ const types = [
36952
+ 'video/mp4; codecs="avc1.42E01E"',
36953
+ 'video/webm; codecs="vp8"',
36954
+ 'video/webm; codecs="vp9"',
36955
+ 'video/ogg; codecs="theora"',
36956
+ 'audio/mp4; codecs="mp4a.40.2"',
36957
+ 'audio/webm; codecs="opus"',
36958
+ ];
36959
+ return types.map((t) => v.canPlayType(t) || '').join('|');
36960
+ }
36961
+ function collectScreen() {
36962
+ const s = window.screen;
36963
+ return [s.width, s.height, s.colorDepth, window.devicePixelRatio || 1].join('x');
36964
+ }
36965
+ async function collectFingerprint() {
36966
+ const [canvasRaw, audioRaw] = await Promise.all([
36967
+ Promise.resolve(collectCanvas()),
36968
+ collectAudio(),
36969
+ ]);
36970
+ const navRaw = collectNav();
36971
+ const fontRaw = collectFonts();
36972
+ const mediaRaw = collectMedia();
36973
+ const screenRaw = collectScreen();
36974
+ const tzCode = Intl.DateTimeFormat().resolvedOptions().timeZone || 'unknown';
36975
+ const langTag = navigator.language || 'unknown';
36976
+ const [canvas_hash, audio_hash, nav_hash, font_hash, media_hash, screen_hash] = await Promise.all([
36977
+ shortHash(canvasRaw),
36978
+ shortHash(audioRaw),
36979
+ shortHash(navRaw).then((h) => h.substring(0, 8)),
36980
+ shortHash(fontRaw),
36981
+ shortHash(mediaRaw),
36982
+ shortHash(screenRaw),
36983
+ ]);
36984
+ const combined = await longHash([canvas_hash, audio_hash, nav_hash, font_hash, media_hash, screen_hash, tzCode, langTag].join('.'));
36985
+ return {
36986
+ canvas_hash,
36987
+ audio_hash,
36988
+ nav_hash,
36989
+ font_hash,
36990
+ media_hash,
36991
+ screen_hash,
36992
+ tz_code: tzCode,
36993
+ lang_tag: langTag,
36994
+ combined,
36995
+ };
36996
+ }
36997
+
36639
36998
  const DEFAULT_BRANDING = {
36640
36999
  name: 'GuardVideo',
36641
37000
  url: 'https://guardvid.com',
@@ -36661,6 +37020,9 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36661
37020
  this.state = exports.PlayerState.IDLE;
36662
37021
  this.embedToken = null;
36663
37022
  this.currentQuality = null;
37023
+ this.eventTracker = null;
37024
+ this.sdkFingerprint = null;
37025
+ this._onSecurityEvent = null;
36664
37026
  this._onRateChange = this.enforceMaxRate.bind(this);
36665
37027
  this.videoElement = videoElement;
36666
37028
  this.config = {
@@ -36678,6 +37040,9 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36678
37040
  viewerName: config.viewerName || '',
36679
37041
  viewerEmail: config.viewerEmail || '',
36680
37042
  forensicWatermark: config.forensicWatermark !== false,
37043
+ trackViewerEvents: config.trackViewerEvents !== false,
37044
+ viewerEventsFlushIntervalMs: config.viewerEventsFlushIntervalMs || 5000,
37045
+ collectViewerFingerprint: config.collectViewerFingerprint !== false,
36681
37046
  onReady: config.onReady || (() => { }),
36682
37047
  onError: config.onError || (() => { }),
36683
37048
  onQualityChange: config.onQualityChange || (() => { }),
@@ -36688,6 +37053,14 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36688
37053
  if (!this.checkAllowedDomain())
36689
37054
  return;
36690
37055
  this.applySecurity();
37056
+ if (this.config.collectViewerFingerprint) {
37057
+ collectFingerprint().then(fp => {
37058
+ this.sdkFingerprint = fp;
37059
+ this.log('Fingerprint collected', fp.combined);
37060
+ }).catch(() => {
37061
+ this.log('Fingerprint collection failed (non-fatal)');
37062
+ });
37063
+ }
36691
37064
  this.initialize();
36692
37065
  }
36693
37066
  log(message, data) {
@@ -36782,6 +37155,9 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36782
37155
  if (cfg.enableWatermark && cfg.watermarkText) {
36783
37156
  this.config.onWatermark?.(cfg.watermarkText);
36784
37157
  }
37158
+ if (this.config.trackViewerEvents && cfg.eventsSecret) {
37159
+ this.initEventTracker(cfg.eventsSecret, cfg.sessionId ?? undefined);
37160
+ }
36785
37161
  }
36786
37162
  catch (err) {
36787
37163
  this.log('fetchAndApplyWatermark error (non-fatal):', err);
@@ -36791,6 +37167,42 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36791
37167
  }
36792
37168
  }
36793
37169
  }
37170
+ initEventTracker(eventsSecret, sessionId) {
37171
+ if (!this.embedToken || !this.config.apiBaseUrl)
37172
+ return;
37173
+ this.eventTracker = new EventTracker({
37174
+ endpoint: this.config.apiBaseUrl + '/videos/viewer-events',
37175
+ tokenId: this.embedToken.tokenId,
37176
+ sessionId,
37177
+ eventsSecret,
37178
+ identityHash: this.sdkFingerprint?.combined,
37179
+ flushIntervalMs: this.config.viewerEventsFlushIntervalMs,
37180
+ debug: this.config.debug,
37181
+ });
37182
+ this.videoElement.addEventListener('play', () => {
37183
+ this.eventTracker?.track('play', this.videoElement.currentTime);
37184
+ this.eventTracker?.startChunk(this.videoElement.currentTime);
37185
+ });
37186
+ this.videoElement.addEventListener('pause', () => {
37187
+ this.eventTracker?.track('pause', this.videoElement.currentTime);
37188
+ this.eventTracker?.endChunk();
37189
+ });
37190
+ this.videoElement.addEventListener('seeked', () => {
37191
+ this.eventTracker?.track('seek', this.videoElement.currentTime);
37192
+ });
37193
+ this.videoElement.addEventListener('ended', () => {
37194
+ this.eventTracker?.track('ended', this.videoElement.currentTime);
37195
+ this.eventTracker?.endChunk();
37196
+ });
37197
+ this.videoElement.addEventListener('timeupdate', () => {
37198
+ this.eventTracker?.tickChunk(this.videoElement.currentTime);
37199
+ });
37200
+ this._onSecurityEvent = ((e) => {
37201
+ this.eventTracker?.track('security_event', this.videoElement.currentTime, e.detail);
37202
+ });
37203
+ document.addEventListener('gv:security', this._onSecurityEvent);
37204
+ this.log('EventTracker initialized');
37205
+ }
36794
37206
  async fetchEmbedToken() {
36795
37207
  const url = this.config.embedTokenEndpoint + '/' + this.videoId;
36796
37208
  this.log('Fetching embed token from', url);
@@ -36804,6 +37216,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36804
37216
  ...(this.config.viewerName ? { viewerName: this.config.viewerName } : {}),
36805
37217
  ...(this.config.viewerEmail ? { viewerEmail: this.config.viewerEmail } : {}),
36806
37218
  forensicWatermark: this.config.forensicWatermark,
37219
+ ...(this.sdkFingerprint ? { sdkFingerprint: this.sdkFingerprint } : {}),
36807
37220
  }),
36808
37221
  });
36809
37222
  if (!response.ok) {
@@ -36951,6 +37364,14 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
36951
37364
  getState() { return this.state; }
36952
37365
  destroy() {
36953
37366
  this.log('Destroying player');
37367
+ if (this.eventTracker) {
37368
+ this.eventTracker.destroy();
37369
+ this.eventTracker = null;
37370
+ }
37371
+ if (this._onSecurityEvent) {
37372
+ document.removeEventListener('gv:security', this._onSecurityEvent);
37373
+ this._onSecurityEvent = null;
37374
+ }
36954
37375
  this.videoElement.removeEventListener('ratechange', this._onRateChange);
36955
37376
  if (this.hls) {
36956
37377
  this.hls.destroy();
@@ -37602,6 +38023,12 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37602
38023
  pointer-events: none;
37603
38024
  letter-spacing: 0.06em;
37604
38025
  }
38026
+ /* ── Canvas watermark layer (Layer 2) ─────────────────────── */
38027
+ .gvp-watermark-canvas {
38028
+ position: absolute; inset: 0;
38029
+ pointer-events: none; z-index: 7;
38030
+ width: 100%; height: 100%;
38031
+ }
37605
38032
 
37606
38033
  /* ── Live dot (for live streams) ─────────────────────────── */
37607
38034
  .gvp-live-badge {
@@ -37767,6 +38194,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37767
38194
  this._ctxKeyDownBound = () => { };
37768
38195
  this._watermarkObserver = null;
37769
38196
  this._watermarkText = '';
38197
+ this._watermarkDriftTimer = null;
37770
38198
  const accent = config.branding?.accentColor ?? '#00e5a0';
37771
38199
  const brandName = config.branding?.name ?? 'GuardVideo';
37772
38200
  injectStyles();
@@ -37787,6 +38215,9 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37787
38215
  this.badge.appendChild(document.createTextNode(brandName));
37788
38216
  this.watermarkDiv = el('div', 'gvp-watermark');
37789
38217
  this.watermarkDiv.setAttribute('aria-hidden', 'true');
38218
+ this.watermarkCanvas = document.createElement('canvas');
38219
+ this.watermarkCanvas.className = 'gvp-watermark-canvas';
38220
+ this.watermarkCanvas.setAttribute('aria-hidden', 'true');
37790
38221
  this.spinner = el('div', 'gvp-spinner gvp-hidden');
37791
38222
  this.spinner.setAttribute('aria-label', 'Loading');
37792
38223
  this.spinner.setAttribute('role', 'status');
@@ -37900,7 +38331,7 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
37900
38331
  btnRow.append(this.playBtn, volWrap, this.timeEl, spacer, speedWrap, divider, qualWrap, this.fsBtn);
37901
38332
  inner.appendChild(btnRow);
37902
38333
  this.controls.appendChild(inner);
37903
- this.root.append(this.videoEl, this.badge, this.watermarkDiv, this.spinner, this.errorOverlay, this.centerPlay, this.clickArea, this.controls);
38334
+ this.root.append(this.videoEl, this.badge, this.watermarkDiv, this.watermarkCanvas, this.spinner, this.errorOverlay, this.centerPlay, this.clickArea, this.controls);
37904
38335
  container.appendChild(this.root);
37905
38336
  this._onFsChangeBound = () => this._onFsChange();
37906
38337
  this._seekMouseMoveBound = (e) => { if (this.seekDragging)
@@ -38332,45 +38763,152 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
38332
38763
  return;
38333
38764
  this._watermarkText = text;
38334
38765
  this.watermarkDiv.innerHTML = '';
38766
+ const seed = this._hashCode(text);
38767
+ const prng = this._mulberry32(seed);
38335
38768
  for (let i = 0; i < 20; i++) {
38336
38769
  const span = el('span', 'gvp-watermark-text');
38337
38770
  span.textContent = text;
38338
- span.style.left = `${(i % 4) * 26 + (Math.floor(i / 4) % 2) * 13}%`;
38339
- span.style.top = `${Math.floor(i / 4) * 22}%`;
38771
+ const baseLeft = (i % 4) * 26 + (Math.floor(i / 4) % 2) * 13;
38772
+ const baseTop = Math.floor(i / 4) * 22;
38773
+ const jitterX = (prng() - 0.5) * 16;
38774
+ const jitterY = (prng() - 0.5) * 10;
38775
+ span.style.left = `${Math.max(0, Math.min(90, baseLeft + jitterX))}%`;
38776
+ span.style.top = `${Math.max(0, Math.min(90, baseTop + jitterY))}%`;
38777
+ const rotJitter = -28 + (prng() - 0.5) * 8;
38778
+ span.style.transform = `rotate(${rotJitter}deg)`;
38779
+ span.style.webkitTransform = `rotate(${rotJitter}deg)`;
38340
38780
  this.watermarkDiv.appendChild(span);
38341
38781
  }
38782
+ this._renderCanvasWatermark(text);
38342
38783
  this._mountWatermarkObserver();
38784
+ this._startWatermarkDrift();
38785
+ }
38786
+ _renderCanvasWatermark(text) {
38787
+ const canvas = this.watermarkCanvas;
38788
+ const w = this.root.clientWidth || 640;
38789
+ const h = this.root.clientHeight || 360;
38790
+ canvas.width = w;
38791
+ canvas.height = h;
38792
+ const ctx = canvas.getContext('2d');
38793
+ if (!ctx)
38794
+ return;
38795
+ ctx.clearRect(0, 0, w, h);
38796
+ ctx.font = '12px monospace';
38797
+ ctx.fillStyle = 'rgba(255,255,255,0.04)';
38798
+ ctx.textBaseline = 'top';
38799
+ const seed = this._hashCode(text);
38800
+ const prng = this._mulberry32(seed);
38801
+ for (let i = 0; i < 20; i++) {
38802
+ const baseLeft = (i % 4) * 26 + (Math.floor(i / 4) % 2) * 13;
38803
+ const baseTop = Math.floor(i / 4) * 22;
38804
+ const jitterX = (prng() - 0.5) * 16;
38805
+ const jitterY = (prng() - 0.5) * 10;
38806
+ const x = Math.max(0, Math.min(90, baseLeft + jitterX)) / 100 * w;
38807
+ const y = Math.max(0, Math.min(90, baseTop + jitterY)) / 100 * h;
38808
+ const rot = (-28 + (prng() - 0.5) * 8) * Math.PI / 180;
38809
+ ctx.save();
38810
+ ctx.translate(x, y);
38811
+ ctx.rotate(rot);
38812
+ ctx.fillText(text, 0, 0);
38813
+ ctx.restore();
38814
+ }
38815
+ }
38816
+ _mulberry32(seed) {
38817
+ let s = seed | 0;
38818
+ return () => {
38819
+ s = (s + 0x6d2b79f5) | 0;
38820
+ let t = Math.imul(s ^ (s >>> 15), 1 | s);
38821
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
38822
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
38823
+ };
38824
+ }
38825
+ _hashCode(s) {
38826
+ let h = 5381;
38827
+ for (let i = 0; i < s.length; i++) {
38828
+ h = ((h << 5) + h + s.charCodeAt(i)) | 0;
38829
+ }
38830
+ return h;
38831
+ }
38832
+ _startWatermarkDrift() {
38833
+ if (this._watermarkDriftTimer)
38834
+ clearInterval(this._watermarkDriftTimer);
38835
+ const seed = this._hashCode(this._watermarkText);
38836
+ const prng = this._mulberry32(seed + Date.now());
38837
+ this._watermarkDriftTimer = setInterval(() => {
38838
+ const spans = this.watermarkDiv.querySelectorAll('.gvp-watermark-text');
38839
+ spans.forEach((span, i) => {
38840
+ const baseLeft = (i % 4) * 26 + (Math.floor(i / 4) % 2) * 13;
38841
+ const baseTop = Math.floor(i / 4) * 22;
38842
+ const jX = (prng() - 0.5) * 16;
38843
+ const jY = (prng() - 0.5) * 10;
38844
+ span.style.transition = 'left 5s ease, top 5s ease';
38845
+ span.style.left = `${Math.max(0, Math.min(90, baseLeft + jX))}%`;
38846
+ span.style.top = `${Math.max(0, Math.min(90, baseTop + jY))}%`;
38847
+ });
38848
+ this._renderCanvasWatermark(this._watermarkText);
38849
+ }, 30000);
38343
38850
  }
38344
38851
  _mountWatermarkObserver() {
38345
38852
  this._watermarkObserver?.disconnect();
38346
38853
  this._watermarkObserver = new MutationObserver((mutations) => {
38854
+ let tampered = false;
38347
38855
  for (const m of mutations) {
38348
38856
  if (m.type === 'childList' && m.target === this.root) {
38349
38857
  if (!this.root.contains(this.watermarkDiv)) {
38350
38858
  this.root.appendChild(this.watermarkDiv);
38859
+ tampered = true;
38860
+ }
38861
+ if (!this.root.contains(this.watermarkCanvas)) {
38862
+ this.root.appendChild(this.watermarkCanvas);
38863
+ tampered = true;
38351
38864
  }
38352
38865
  }
38353
38866
  if (m.type === 'attributes' && m.target === this.watermarkDiv) {
38354
38867
  this.watermarkDiv.removeAttribute('style');
38355
38868
  this.watermarkDiv.className = 'gvp-watermark';
38356
38869
  this.watermarkDiv.setAttribute('aria-hidden', 'true');
38870
+ tampered = true;
38871
+ }
38872
+ if (m.type === 'attributes' && m.target === this.watermarkCanvas) {
38873
+ this.watermarkCanvas.className = 'gvp-watermark-canvas';
38874
+ this.watermarkCanvas.setAttribute('aria-hidden', 'true');
38875
+ this._renderCanvasWatermark(this._watermarkText);
38876
+ tampered = true;
38357
38877
  }
38358
38878
  if (m.type === 'childList' && m.target === this.watermarkDiv) {
38359
38879
  if (this.watermarkDiv.childElementCount < 20) {
38360
38880
  this._refillWatermark();
38881
+ tampered = true;
38361
38882
  }
38362
38883
  }
38363
38884
  }
38885
+ if (tampered) {
38886
+ this.root.dispatchEvent(new CustomEvent('gv:security', {
38887
+ bubbles: true,
38888
+ detail: { type: 'watermark_tamper', timestamp: Date.now() },
38889
+ }));
38890
+ }
38364
38891
  });
38365
38892
  this._watermarkObserver.observe(this.root, { childList: true });
38366
38893
  this._watermarkObserver.observe(this.watermarkDiv, { attributes: true, childList: true });
38367
38894
  }
38368
38895
  _refillWatermark() {
38896
+ const seed = this._hashCode(this._watermarkText);
38897
+ const prng = this._mulberry32(seed);
38898
+ for (let j = 0; j < this.watermarkDiv.childElementCount * 3; j++)
38899
+ prng();
38369
38900
  for (let i = this.watermarkDiv.childElementCount; i < 20; i++) {
38370
38901
  const span = el('span', 'gvp-watermark-text');
38371
38902
  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}%`;
38903
+ const baseLeft = (i % 4) * 26 + (Math.floor(i / 4) % 2) * 13;
38904
+ const baseTop = Math.floor(i / 4) * 22;
38905
+ const jX = (prng() - 0.5) * 16;
38906
+ const jY = (prng() - 0.5) * 10;
38907
+ span.style.left = `${Math.max(0, Math.min(90, baseLeft + jX))}%`;
38908
+ span.style.top = `${Math.max(0, Math.min(90, baseTop + jY))}%`;
38909
+ const rotJitter = -28 + (prng() - 0.5) * 8;
38910
+ span.style.transform = `rotate(${rotJitter}deg)`;
38911
+ span.style.webkitTransform = `rotate(${rotJitter}deg)`;
38374
38912
  this.watermarkDiv.appendChild(span);
38375
38913
  }
38376
38914
  }
@@ -38561,6 +39099,8 @@ Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timeline
38561
39099
  document.removeEventListener('keydown', this._ctxKeyDownBound);
38562
39100
  this._hideContextMenu();
38563
39101
  this._watermarkObserver?.disconnect();
39102
+ if (this._watermarkDriftTimer)
39103
+ clearInterval(this._watermarkDriftTimer);
38564
39104
  this.corePlayer.destroy();
38565
39105
  this.root.remove();
38566
39106
  }