@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.
- package/dist/core/EventTracker.d.ts +38 -0
- package/dist/core/EventTracker.d.ts.map +1 -0
- package/dist/core/ViewerFingerprint.d.ts +13 -0
- package/dist/core/ViewerFingerprint.d.ts.map +1 -0
- package/dist/core/WatchChunkAccumulator.d.ts +20 -0
- package/dist/core/WatchChunkAccumulator.d.ts.map +1 -0
- package/dist/core/player.d.ts +4 -0
- package/dist/core/player.d.ts.map +1 -1
- package/dist/core/types.d.ts +4 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +546 -6
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +548 -5
- package/dist/index.js.map +1 -1
- package/dist/ui/PlayerUI.d.ts +6 -0
- package/dist/ui/PlayerUI.d.ts.map +1 -1
- package/dist/vanilla/guardvideo-player.js +545 -5
- package/dist/vanilla/guardvideo-player.js.map +1 -1
- package/dist/vanilla/guardvideo-player.min.js +1 -1
- package/dist/vanilla/guardvideo-player.min.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
38339
|
-
|
|
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
|
-
|
|
38373
|
-
|
|
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
|
}
|