@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
package/dist/index.js
CHANGED
|
@@ -14,6 +14,365 @@ exports.PlayerState = void 0;
|
|
|
14
14
|
PlayerState["ERROR"] = "error";
|
|
15
15
|
})(exports.PlayerState || (exports.PlayerState = {}));
|
|
16
16
|
|
|
17
|
+
const MAX_CHUNK_SECONDS = 30;
|
|
18
|
+
class WatchChunkAccumulator {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.chunkStart = null;
|
|
21
|
+
this.lastPosition = 0;
|
|
22
|
+
this.pendingChunks = [];
|
|
23
|
+
}
|
|
24
|
+
startChunk(positionSeconds) {
|
|
25
|
+
if (this.chunkStart === null) {
|
|
26
|
+
this.chunkStart = positionSeconds;
|
|
27
|
+
this.lastPosition = positionSeconds;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
tick(positionSeconds) {
|
|
31
|
+
if (this.chunkStart === null)
|
|
32
|
+
return;
|
|
33
|
+
this.lastPosition = positionSeconds;
|
|
34
|
+
const elapsed = positionSeconds - this.chunkStart;
|
|
35
|
+
if (elapsed >= MAX_CHUNK_SECONDS) {
|
|
36
|
+
this.finalizeChunk();
|
|
37
|
+
this.chunkStart = positionSeconds;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
finalizeChunk() {
|
|
41
|
+
if (this.chunkStart === null)
|
|
42
|
+
return null;
|
|
43
|
+
const duration = this.lastPosition - this.chunkStart;
|
|
44
|
+
if (duration < 0.5) {
|
|
45
|
+
this.chunkStart = null;
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const chunk = {
|
|
49
|
+
event_type: 'watch_chunk',
|
|
50
|
+
event_at: new Date().toISOString(),
|
|
51
|
+
position_seconds: this.lastPosition,
|
|
52
|
+
payload: {
|
|
53
|
+
start_seconds: this.chunkStart,
|
|
54
|
+
duration_seconds: Math.round(duration * 100) / 100,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
this.pendingChunks.push(chunk);
|
|
58
|
+
this.chunkStart = null;
|
|
59
|
+
return chunk;
|
|
60
|
+
}
|
|
61
|
+
drain() {
|
|
62
|
+
const chunks = [...this.pendingChunks];
|
|
63
|
+
this.pendingChunks = [];
|
|
64
|
+
return chunks;
|
|
65
|
+
}
|
|
66
|
+
reset() {
|
|
67
|
+
this.chunkStart = null;
|
|
68
|
+
this.lastPosition = 0;
|
|
69
|
+
this.pendingChunks = [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const MAX_BATCH_SIZE = 25;
|
|
74
|
+
const DEFAULT_FLUSH_INTERVAL = 5000;
|
|
75
|
+
class EventTracker {
|
|
76
|
+
constructor(config) {
|
|
77
|
+
this.buffer = [];
|
|
78
|
+
this.flushTimer = null;
|
|
79
|
+
this.accumulator = new WatchChunkAccumulator();
|
|
80
|
+
this.destroyed = false;
|
|
81
|
+
this._onBeforeUnload = null;
|
|
82
|
+
this.config = {
|
|
83
|
+
endpoint: config.endpoint,
|
|
84
|
+
tokenId: config.tokenId,
|
|
85
|
+
sessionId: config.sessionId || '',
|
|
86
|
+
eventsSecret: config.eventsSecret,
|
|
87
|
+
identityHash: config.identityHash || '',
|
|
88
|
+
flushIntervalMs: config.flushIntervalMs || DEFAULT_FLUSH_INTERVAL,
|
|
89
|
+
debug: config.debug || false,
|
|
90
|
+
};
|
|
91
|
+
this.startAutoFlush();
|
|
92
|
+
this.hookPageUnload();
|
|
93
|
+
}
|
|
94
|
+
track(type, positionSeconds, payload) {
|
|
95
|
+
if (this.destroyed)
|
|
96
|
+
return;
|
|
97
|
+
this.buffer.push({
|
|
98
|
+
event_type: type,
|
|
99
|
+
event_at: new Date().toISOString(),
|
|
100
|
+
position_seconds: positionSeconds,
|
|
101
|
+
payload,
|
|
102
|
+
});
|
|
103
|
+
if (this.buffer.length >= MAX_BATCH_SIZE) {
|
|
104
|
+
this.flush();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
startChunk(position) {
|
|
108
|
+
this.accumulator.startChunk(position);
|
|
109
|
+
}
|
|
110
|
+
tickChunk(position) {
|
|
111
|
+
this.accumulator.tick(position);
|
|
112
|
+
}
|
|
113
|
+
endChunk() {
|
|
114
|
+
this.accumulator.finalizeChunk();
|
|
115
|
+
}
|
|
116
|
+
setSessionId(sessionId) {
|
|
117
|
+
this.config.sessionId = sessionId;
|
|
118
|
+
}
|
|
119
|
+
async flush() {
|
|
120
|
+
if (this.destroyed)
|
|
121
|
+
return;
|
|
122
|
+
const chunks = this.accumulator.drain();
|
|
123
|
+
for (const c of chunks) {
|
|
124
|
+
this.buffer.push(c);
|
|
125
|
+
}
|
|
126
|
+
if (this.buffer.length === 0)
|
|
127
|
+
return;
|
|
128
|
+
const batch = this.buffer.splice(0, MAX_BATCH_SIZE);
|
|
129
|
+
await this.sendBatch(batch);
|
|
130
|
+
}
|
|
131
|
+
destroy() {
|
|
132
|
+
this.destroyed = true;
|
|
133
|
+
this.accumulator.finalizeChunk();
|
|
134
|
+
this.flush();
|
|
135
|
+
if (this.flushTimer)
|
|
136
|
+
clearInterval(this.flushTimer);
|
|
137
|
+
if (this._onBeforeUnload) {
|
|
138
|
+
window.removeEventListener('beforeunload', this._onBeforeUnload);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
startAutoFlush() {
|
|
142
|
+
this.flushTimer = setInterval(() => this.flush(), this.config.flushIntervalMs);
|
|
143
|
+
}
|
|
144
|
+
hookPageUnload() {
|
|
145
|
+
if (typeof window === 'undefined')
|
|
146
|
+
return;
|
|
147
|
+
this._onBeforeUnload = () => {
|
|
148
|
+
this.accumulator.finalizeChunk();
|
|
149
|
+
const chunks = this.accumulator.drain();
|
|
150
|
+
const allEvents = [...this.buffer, ...chunks];
|
|
151
|
+
if (allEvents.length === 0)
|
|
152
|
+
return;
|
|
153
|
+
const body = JSON.stringify({
|
|
154
|
+
tokenId: this.config.tokenId,
|
|
155
|
+
sessionId: this.config.sessionId || undefined,
|
|
156
|
+
identityHash: this.config.identityHash || undefined,
|
|
157
|
+
events: allEvents.slice(0, MAX_BATCH_SIZE),
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
navigator.sendBeacon(this.config.endpoint, new Blob([body], { type: 'application/json' }));
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
window.addEventListener('beforeunload', this._onBeforeUnload);
|
|
166
|
+
}
|
|
167
|
+
async sendBatch(events) {
|
|
168
|
+
const body = JSON.stringify({
|
|
169
|
+
tokenId: this.config.tokenId,
|
|
170
|
+
sessionId: this.config.sessionId || undefined,
|
|
171
|
+
identityHash: this.config.identityHash || undefined,
|
|
172
|
+
events,
|
|
173
|
+
});
|
|
174
|
+
const nonce = this.randomNonce();
|
|
175
|
+
const timestamp = String(Date.now());
|
|
176
|
+
const signature = await this.sign(nonce, timestamp, body);
|
|
177
|
+
try {
|
|
178
|
+
const resp = await fetch(this.config.endpoint, {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: {
|
|
181
|
+
'Content-Type': 'application/json',
|
|
182
|
+
'X-GV-Nonce': nonce,
|
|
183
|
+
'X-GV-Timestamp': timestamp,
|
|
184
|
+
'X-GV-Signature': signature,
|
|
185
|
+
},
|
|
186
|
+
body,
|
|
187
|
+
credentials: 'omit',
|
|
188
|
+
});
|
|
189
|
+
if (!resp.ok && this.config.debug) {
|
|
190
|
+
console.warn('[GuardVideo EventTracker] Flush failed:', resp.status);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
if (this.config.debug) {
|
|
195
|
+
console.warn('[GuardVideo EventTracker] Flush error:', err);
|
|
196
|
+
}
|
|
197
|
+
this.buffer.unshift(...events);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
randomNonce() {
|
|
201
|
+
const arr = new Uint8Array(8);
|
|
202
|
+
crypto.getRandomValues(arr);
|
|
203
|
+
return Array.from(arr)
|
|
204
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
205
|
+
.join('');
|
|
206
|
+
}
|
|
207
|
+
async sign(nonce, timestamp, body) {
|
|
208
|
+
const enc = new TextEncoder();
|
|
209
|
+
const key = await crypto.subtle.importKey('raw', enc.encode(this.config.eventsSecret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
210
|
+
const message = `${nonce}.${timestamp}.${body}`;
|
|
211
|
+
const sig = await crypto.subtle.sign('HMAC', key, enc.encode(message));
|
|
212
|
+
return Array.from(new Uint8Array(sig))
|
|
213
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
214
|
+
.join('');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function shortHash(input) {
|
|
219
|
+
const buf = new TextEncoder().encode(input);
|
|
220
|
+
const digest = await crypto.subtle.digest('SHA-256', buf);
|
|
221
|
+
return Array.from(new Uint8Array(digest))
|
|
222
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
223
|
+
.join('')
|
|
224
|
+
.substring(0, 16);
|
|
225
|
+
}
|
|
226
|
+
async function longHash(input) {
|
|
227
|
+
const buf = new TextEncoder().encode(input);
|
|
228
|
+
const digest = await crypto.subtle.digest('SHA-256', buf);
|
|
229
|
+
return Array.from(new Uint8Array(digest))
|
|
230
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
231
|
+
.join('')
|
|
232
|
+
.substring(0, 32);
|
|
233
|
+
}
|
|
234
|
+
function collectCanvas() {
|
|
235
|
+
try {
|
|
236
|
+
const c = document.createElement('canvas');
|
|
237
|
+
c.width = 200;
|
|
238
|
+
c.height = 50;
|
|
239
|
+
const ctx = c.getContext('2d');
|
|
240
|
+
if (!ctx)
|
|
241
|
+
return 'no-canvas';
|
|
242
|
+
ctx.textBaseline = 'top';
|
|
243
|
+
ctx.font = '14px Arial';
|
|
244
|
+
ctx.fillStyle = '#f60';
|
|
245
|
+
ctx.fillRect(125, 1, 62, 20);
|
|
246
|
+
ctx.fillStyle = '#069';
|
|
247
|
+
ctx.fillText('GuardVideo<canvas>', 2, 15);
|
|
248
|
+
ctx.fillStyle = 'rgba(102,204,0,0.7)';
|
|
249
|
+
ctx.fillText('fingerprint', 4, 35);
|
|
250
|
+
return c.toDataURL();
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return 'canvas-error';
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async function collectAudio() {
|
|
257
|
+
try {
|
|
258
|
+
const ctx = new (window.OfflineAudioContext ||
|
|
259
|
+
window.webkitOfflineAudioContext)(1, 44100, 44100);
|
|
260
|
+
const osc = ctx.createOscillator();
|
|
261
|
+
osc.type = 'triangle';
|
|
262
|
+
osc.frequency.setValueAtTime(10000, ctx.currentTime);
|
|
263
|
+
const comp = ctx.createDynamicsCompressor();
|
|
264
|
+
comp.threshold.setValueAtTime(-50, ctx.currentTime);
|
|
265
|
+
comp.knee.setValueAtTime(40, ctx.currentTime);
|
|
266
|
+
comp.ratio.setValueAtTime(12, ctx.currentTime);
|
|
267
|
+
comp.attack.setValueAtTime(0, ctx.currentTime);
|
|
268
|
+
comp.release.setValueAtTime(0.25, ctx.currentTime);
|
|
269
|
+
osc.connect(comp);
|
|
270
|
+
comp.connect(ctx.destination);
|
|
271
|
+
osc.start(0);
|
|
272
|
+
const rendered = await ctx.startRendering();
|
|
273
|
+
const data = rendered.getChannelData(0);
|
|
274
|
+
let sum = 0;
|
|
275
|
+
for (let i = 4500; i < 5000; i++)
|
|
276
|
+
sum += Math.abs(data[i]);
|
|
277
|
+
return sum.toString();
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return 'audio-error';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function collectNav() {
|
|
284
|
+
const n = navigator;
|
|
285
|
+
return [
|
|
286
|
+
n.hardwareConcurrency || 0,
|
|
287
|
+
n.deviceMemory || 0,
|
|
288
|
+
n.maxTouchPoints || 0,
|
|
289
|
+
n.platform || '',
|
|
290
|
+
n.userAgentData?.platform || '',
|
|
291
|
+
n.userAgentData?.mobile ? '1' : '0',
|
|
292
|
+
].join('|');
|
|
293
|
+
}
|
|
294
|
+
function collectFonts() {
|
|
295
|
+
try {
|
|
296
|
+
const baseFonts = ['monospace', 'sans-serif', 'serif'];
|
|
297
|
+
const testFonts = [
|
|
298
|
+
'Arial', 'Courier New', 'Georgia', 'Times New Roman',
|
|
299
|
+
'Verdana', 'Trebuchet MS', 'Palatino', 'Impact', 'Comic Sans MS',
|
|
300
|
+
];
|
|
301
|
+
const span = document.createElement('span');
|
|
302
|
+
span.style.cssText = 'position:absolute;left:-9999px;font-size:72px;visibility:hidden';
|
|
303
|
+
span.textContent = 'mmmmmmmmmmlli';
|
|
304
|
+
document.body.appendChild(span);
|
|
305
|
+
const baseSizes = {};
|
|
306
|
+
for (const bf of baseFonts) {
|
|
307
|
+
span.style.fontFamily = bf;
|
|
308
|
+
baseSizes[bf] = span.offsetWidth;
|
|
309
|
+
}
|
|
310
|
+
const detected = [];
|
|
311
|
+
for (const tf of testFonts) {
|
|
312
|
+
for (const bf of baseFonts) {
|
|
313
|
+
span.style.fontFamily = `'${tf}', ${bf}`;
|
|
314
|
+
if (span.offsetWidth !== baseSizes[bf]) {
|
|
315
|
+
detected.push(tf);
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
document.body.removeChild(span);
|
|
321
|
+
return detected.join(',');
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return 'font-error';
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function collectMedia() {
|
|
328
|
+
const v = document.createElement('video');
|
|
329
|
+
const types = [
|
|
330
|
+
'video/mp4; codecs="avc1.42E01E"',
|
|
331
|
+
'video/webm; codecs="vp8"',
|
|
332
|
+
'video/webm; codecs="vp9"',
|
|
333
|
+
'video/ogg; codecs="theora"',
|
|
334
|
+
'audio/mp4; codecs="mp4a.40.2"',
|
|
335
|
+
'audio/webm; codecs="opus"',
|
|
336
|
+
];
|
|
337
|
+
return types.map((t) => v.canPlayType(t) || '').join('|');
|
|
338
|
+
}
|
|
339
|
+
function collectScreen() {
|
|
340
|
+
const s = window.screen;
|
|
341
|
+
return [s.width, s.height, s.colorDepth, window.devicePixelRatio || 1].join('x');
|
|
342
|
+
}
|
|
343
|
+
async function collectFingerprint() {
|
|
344
|
+
const [canvasRaw, audioRaw] = await Promise.all([
|
|
345
|
+
Promise.resolve(collectCanvas()),
|
|
346
|
+
collectAudio(),
|
|
347
|
+
]);
|
|
348
|
+
const navRaw = collectNav();
|
|
349
|
+
const fontRaw = collectFonts();
|
|
350
|
+
const mediaRaw = collectMedia();
|
|
351
|
+
const screenRaw = collectScreen();
|
|
352
|
+
const tzCode = Intl.DateTimeFormat().resolvedOptions().timeZone || 'unknown';
|
|
353
|
+
const langTag = navigator.language || 'unknown';
|
|
354
|
+
const [canvas_hash, audio_hash, nav_hash, font_hash, media_hash, screen_hash] = await Promise.all([
|
|
355
|
+
shortHash(canvasRaw),
|
|
356
|
+
shortHash(audioRaw),
|
|
357
|
+
shortHash(navRaw).then((h) => h.substring(0, 8)),
|
|
358
|
+
shortHash(fontRaw),
|
|
359
|
+
shortHash(mediaRaw),
|
|
360
|
+
shortHash(screenRaw),
|
|
361
|
+
]);
|
|
362
|
+
const combined = await longHash([canvas_hash, audio_hash, nav_hash, font_hash, media_hash, screen_hash, tzCode, langTag].join('.'));
|
|
363
|
+
return {
|
|
364
|
+
canvas_hash,
|
|
365
|
+
audio_hash,
|
|
366
|
+
nav_hash,
|
|
367
|
+
font_hash,
|
|
368
|
+
media_hash,
|
|
369
|
+
screen_hash,
|
|
370
|
+
tz_code: tzCode,
|
|
371
|
+
lang_tag: langTag,
|
|
372
|
+
combined,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
17
376
|
const DEFAULT_BRANDING = {
|
|
18
377
|
name: 'GuardVideo',
|
|
19
378
|
url: 'https://guardvid.com',
|
|
@@ -39,6 +398,9 @@ class GuardVideoPlayer {
|
|
|
39
398
|
this.state = exports.PlayerState.IDLE;
|
|
40
399
|
this.embedToken = null;
|
|
41
400
|
this.currentQuality = null;
|
|
401
|
+
this.eventTracker = null;
|
|
402
|
+
this.sdkFingerprint = null;
|
|
403
|
+
this._onSecurityEvent = null;
|
|
42
404
|
this._onRateChange = this.enforceMaxRate.bind(this);
|
|
43
405
|
this.videoElement = videoElement;
|
|
44
406
|
this.config = {
|
|
@@ -56,6 +418,9 @@ class GuardVideoPlayer {
|
|
|
56
418
|
viewerName: config.viewerName || '',
|
|
57
419
|
viewerEmail: config.viewerEmail || '',
|
|
58
420
|
forensicWatermark: config.forensicWatermark !== false,
|
|
421
|
+
trackViewerEvents: config.trackViewerEvents !== false,
|
|
422
|
+
viewerEventsFlushIntervalMs: config.viewerEventsFlushIntervalMs || 5000,
|
|
423
|
+
collectViewerFingerprint: config.collectViewerFingerprint !== false,
|
|
59
424
|
onReady: config.onReady || (() => { }),
|
|
60
425
|
onError: config.onError || (() => { }),
|
|
61
426
|
onQualityChange: config.onQualityChange || (() => { }),
|
|
@@ -66,6 +431,14 @@ class GuardVideoPlayer {
|
|
|
66
431
|
if (!this.checkAllowedDomain())
|
|
67
432
|
return;
|
|
68
433
|
this.applySecurity();
|
|
434
|
+
if (this.config.collectViewerFingerprint) {
|
|
435
|
+
collectFingerprint().then(fp => {
|
|
436
|
+
this.sdkFingerprint = fp;
|
|
437
|
+
this.log('Fingerprint collected', fp.combined);
|
|
438
|
+
}).catch(() => {
|
|
439
|
+
this.log('Fingerprint collection failed (non-fatal)');
|
|
440
|
+
});
|
|
441
|
+
}
|
|
69
442
|
this.initialize();
|
|
70
443
|
}
|
|
71
444
|
log(message, data) {
|
|
@@ -160,6 +533,9 @@ class GuardVideoPlayer {
|
|
|
160
533
|
if (cfg.enableWatermark && cfg.watermarkText) {
|
|
161
534
|
this.config.onWatermark?.(cfg.watermarkText);
|
|
162
535
|
}
|
|
536
|
+
if (this.config.trackViewerEvents && cfg.eventsSecret) {
|
|
537
|
+
this.initEventTracker(cfg.eventsSecret, cfg.sessionId ?? undefined);
|
|
538
|
+
}
|
|
163
539
|
}
|
|
164
540
|
catch (err) {
|
|
165
541
|
this.log('fetchAndApplyWatermark error (non-fatal):', err);
|
|
@@ -169,6 +545,42 @@ class GuardVideoPlayer {
|
|
|
169
545
|
}
|
|
170
546
|
}
|
|
171
547
|
}
|
|
548
|
+
initEventTracker(eventsSecret, sessionId) {
|
|
549
|
+
if (!this.embedToken || !this.config.apiBaseUrl)
|
|
550
|
+
return;
|
|
551
|
+
this.eventTracker = new EventTracker({
|
|
552
|
+
endpoint: this.config.apiBaseUrl + '/videos/viewer-events',
|
|
553
|
+
tokenId: this.embedToken.tokenId,
|
|
554
|
+
sessionId,
|
|
555
|
+
eventsSecret,
|
|
556
|
+
identityHash: this.sdkFingerprint?.combined,
|
|
557
|
+
flushIntervalMs: this.config.viewerEventsFlushIntervalMs,
|
|
558
|
+
debug: this.config.debug,
|
|
559
|
+
});
|
|
560
|
+
this.videoElement.addEventListener('play', () => {
|
|
561
|
+
this.eventTracker?.track('play', this.videoElement.currentTime);
|
|
562
|
+
this.eventTracker?.startChunk(this.videoElement.currentTime);
|
|
563
|
+
});
|
|
564
|
+
this.videoElement.addEventListener('pause', () => {
|
|
565
|
+
this.eventTracker?.track('pause', this.videoElement.currentTime);
|
|
566
|
+
this.eventTracker?.endChunk();
|
|
567
|
+
});
|
|
568
|
+
this.videoElement.addEventListener('seeked', () => {
|
|
569
|
+
this.eventTracker?.track('seek', this.videoElement.currentTime);
|
|
570
|
+
});
|
|
571
|
+
this.videoElement.addEventListener('ended', () => {
|
|
572
|
+
this.eventTracker?.track('ended', this.videoElement.currentTime);
|
|
573
|
+
this.eventTracker?.endChunk();
|
|
574
|
+
});
|
|
575
|
+
this.videoElement.addEventListener('timeupdate', () => {
|
|
576
|
+
this.eventTracker?.tickChunk(this.videoElement.currentTime);
|
|
577
|
+
});
|
|
578
|
+
this._onSecurityEvent = ((e) => {
|
|
579
|
+
this.eventTracker?.track('security_event', this.videoElement.currentTime, e.detail);
|
|
580
|
+
});
|
|
581
|
+
document.addEventListener('gv:security', this._onSecurityEvent);
|
|
582
|
+
this.log('EventTracker initialized');
|
|
583
|
+
}
|
|
172
584
|
async fetchEmbedToken() {
|
|
173
585
|
const url = this.config.embedTokenEndpoint + '/' + this.videoId;
|
|
174
586
|
this.log('Fetching embed token from', url);
|
|
@@ -182,6 +594,7 @@ class GuardVideoPlayer {
|
|
|
182
594
|
...(this.config.viewerName ? { viewerName: this.config.viewerName } : {}),
|
|
183
595
|
...(this.config.viewerEmail ? { viewerEmail: this.config.viewerEmail } : {}),
|
|
184
596
|
forensicWatermark: this.config.forensicWatermark,
|
|
597
|
+
...(this.sdkFingerprint ? { sdkFingerprint: this.sdkFingerprint } : {}),
|
|
185
598
|
}),
|
|
186
599
|
});
|
|
187
600
|
if (!response.ok) {
|
|
@@ -329,6 +742,14 @@ class GuardVideoPlayer {
|
|
|
329
742
|
getState() { return this.state; }
|
|
330
743
|
destroy() {
|
|
331
744
|
this.log('Destroying player');
|
|
745
|
+
if (this.eventTracker) {
|
|
746
|
+
this.eventTracker.destroy();
|
|
747
|
+
this.eventTracker = null;
|
|
748
|
+
}
|
|
749
|
+
if (this._onSecurityEvent) {
|
|
750
|
+
document.removeEventListener('gv:security', this._onSecurityEvent);
|
|
751
|
+
this._onSecurityEvent = null;
|
|
752
|
+
}
|
|
332
753
|
this.videoElement.removeEventListener('ratechange', this._onRateChange);
|
|
333
754
|
if (this.hls) {
|
|
334
755
|
this.hls.destroy();
|
|
@@ -980,6 +1401,12 @@ function injectStyles() {
|
|
|
980
1401
|
pointer-events: none;
|
|
981
1402
|
letter-spacing: 0.06em;
|
|
982
1403
|
}
|
|
1404
|
+
/* ── Canvas watermark layer (Layer 2) ─────────────────────── */
|
|
1405
|
+
.gvp-watermark-canvas {
|
|
1406
|
+
position: absolute; inset: 0;
|
|
1407
|
+
pointer-events: none; z-index: 7;
|
|
1408
|
+
width: 100%; height: 100%;
|
|
1409
|
+
}
|
|
983
1410
|
|
|
984
1411
|
/* ── Live dot (for live streams) ─────────────────────────── */
|
|
985
1412
|
.gvp-live-badge {
|
|
@@ -1145,6 +1572,7 @@ class PlayerUI {
|
|
|
1145
1572
|
this._ctxKeyDownBound = () => { };
|
|
1146
1573
|
this._watermarkObserver = null;
|
|
1147
1574
|
this._watermarkText = '';
|
|
1575
|
+
this._watermarkDriftTimer = null;
|
|
1148
1576
|
const accent = config.branding?.accentColor ?? '#00e5a0';
|
|
1149
1577
|
const brandName = config.branding?.name ?? 'GuardVideo';
|
|
1150
1578
|
injectStyles();
|
|
@@ -1165,6 +1593,9 @@ class PlayerUI {
|
|
|
1165
1593
|
this.badge.appendChild(document.createTextNode(brandName));
|
|
1166
1594
|
this.watermarkDiv = el('div', 'gvp-watermark');
|
|
1167
1595
|
this.watermarkDiv.setAttribute('aria-hidden', 'true');
|
|
1596
|
+
this.watermarkCanvas = document.createElement('canvas');
|
|
1597
|
+
this.watermarkCanvas.className = 'gvp-watermark-canvas';
|
|
1598
|
+
this.watermarkCanvas.setAttribute('aria-hidden', 'true');
|
|
1168
1599
|
this.spinner = el('div', 'gvp-spinner gvp-hidden');
|
|
1169
1600
|
this.spinner.setAttribute('aria-label', 'Loading');
|
|
1170
1601
|
this.spinner.setAttribute('role', 'status');
|
|
@@ -1278,7 +1709,7 @@ class PlayerUI {
|
|
|
1278
1709
|
btnRow.append(this.playBtn, volWrap, this.timeEl, spacer, speedWrap, divider, qualWrap, this.fsBtn);
|
|
1279
1710
|
inner.appendChild(btnRow);
|
|
1280
1711
|
this.controls.appendChild(inner);
|
|
1281
|
-
this.root.append(this.videoEl, this.badge, this.watermarkDiv, this.spinner, this.errorOverlay, this.centerPlay, this.clickArea, this.controls);
|
|
1712
|
+
this.root.append(this.videoEl, this.badge, this.watermarkDiv, this.watermarkCanvas, this.spinner, this.errorOverlay, this.centerPlay, this.clickArea, this.controls);
|
|
1282
1713
|
container.appendChild(this.root);
|
|
1283
1714
|
this._onFsChangeBound = () => this._onFsChange();
|
|
1284
1715
|
this._seekMouseMoveBound = (e) => { if (this.seekDragging)
|
|
@@ -1710,45 +2141,152 @@ class PlayerUI {
|
|
|
1710
2141
|
return;
|
|
1711
2142
|
this._watermarkText = text;
|
|
1712
2143
|
this.watermarkDiv.innerHTML = '';
|
|
2144
|
+
const seed = this._hashCode(text);
|
|
2145
|
+
const prng = this._mulberry32(seed);
|
|
1713
2146
|
for (let i = 0; i < 20; i++) {
|
|
1714
2147
|
const span = el('span', 'gvp-watermark-text');
|
|
1715
2148
|
span.textContent = text;
|
|
1716
|
-
|
|
1717
|
-
|
|
2149
|
+
const baseLeft = (i % 4) * 26 + (Math.floor(i / 4) % 2) * 13;
|
|
2150
|
+
const baseTop = Math.floor(i / 4) * 22;
|
|
2151
|
+
const jitterX = (prng() - 0.5) * 16;
|
|
2152
|
+
const jitterY = (prng() - 0.5) * 10;
|
|
2153
|
+
span.style.left = `${Math.max(0, Math.min(90, baseLeft + jitterX))}%`;
|
|
2154
|
+
span.style.top = `${Math.max(0, Math.min(90, baseTop + jitterY))}%`;
|
|
2155
|
+
const rotJitter = -28 + (prng() - 0.5) * 8;
|
|
2156
|
+
span.style.transform = `rotate(${rotJitter}deg)`;
|
|
2157
|
+
span.style.webkitTransform = `rotate(${rotJitter}deg)`;
|
|
1718
2158
|
this.watermarkDiv.appendChild(span);
|
|
1719
2159
|
}
|
|
2160
|
+
this._renderCanvasWatermark(text);
|
|
1720
2161
|
this._mountWatermarkObserver();
|
|
2162
|
+
this._startWatermarkDrift();
|
|
2163
|
+
}
|
|
2164
|
+
_renderCanvasWatermark(text) {
|
|
2165
|
+
const canvas = this.watermarkCanvas;
|
|
2166
|
+
const w = this.root.clientWidth || 640;
|
|
2167
|
+
const h = this.root.clientHeight || 360;
|
|
2168
|
+
canvas.width = w;
|
|
2169
|
+
canvas.height = h;
|
|
2170
|
+
const ctx = canvas.getContext('2d');
|
|
2171
|
+
if (!ctx)
|
|
2172
|
+
return;
|
|
2173
|
+
ctx.clearRect(0, 0, w, h);
|
|
2174
|
+
ctx.font = '12px monospace';
|
|
2175
|
+
ctx.fillStyle = 'rgba(255,255,255,0.04)';
|
|
2176
|
+
ctx.textBaseline = 'top';
|
|
2177
|
+
const seed = this._hashCode(text);
|
|
2178
|
+
const prng = this._mulberry32(seed);
|
|
2179
|
+
for (let i = 0; i < 20; i++) {
|
|
2180
|
+
const baseLeft = (i % 4) * 26 + (Math.floor(i / 4) % 2) * 13;
|
|
2181
|
+
const baseTop = Math.floor(i / 4) * 22;
|
|
2182
|
+
const jitterX = (prng() - 0.5) * 16;
|
|
2183
|
+
const jitterY = (prng() - 0.5) * 10;
|
|
2184
|
+
const x = Math.max(0, Math.min(90, baseLeft + jitterX)) / 100 * w;
|
|
2185
|
+
const y = Math.max(0, Math.min(90, baseTop + jitterY)) / 100 * h;
|
|
2186
|
+
const rot = (-28 + (prng() - 0.5) * 8) * Math.PI / 180;
|
|
2187
|
+
ctx.save();
|
|
2188
|
+
ctx.translate(x, y);
|
|
2189
|
+
ctx.rotate(rot);
|
|
2190
|
+
ctx.fillText(text, 0, 0);
|
|
2191
|
+
ctx.restore();
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
_mulberry32(seed) {
|
|
2195
|
+
let s = seed | 0;
|
|
2196
|
+
return () => {
|
|
2197
|
+
s = (s + 0x6d2b79f5) | 0;
|
|
2198
|
+
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
|
2199
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
2200
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
_hashCode(s) {
|
|
2204
|
+
let h = 5381;
|
|
2205
|
+
for (let i = 0; i < s.length; i++) {
|
|
2206
|
+
h = ((h << 5) + h + s.charCodeAt(i)) | 0;
|
|
2207
|
+
}
|
|
2208
|
+
return h;
|
|
2209
|
+
}
|
|
2210
|
+
_startWatermarkDrift() {
|
|
2211
|
+
if (this._watermarkDriftTimer)
|
|
2212
|
+
clearInterval(this._watermarkDriftTimer);
|
|
2213
|
+
const seed = this._hashCode(this._watermarkText);
|
|
2214
|
+
const prng = this._mulberry32(seed + Date.now());
|
|
2215
|
+
this._watermarkDriftTimer = setInterval(() => {
|
|
2216
|
+
const spans = this.watermarkDiv.querySelectorAll('.gvp-watermark-text');
|
|
2217
|
+
spans.forEach((span, i) => {
|
|
2218
|
+
const baseLeft = (i % 4) * 26 + (Math.floor(i / 4) % 2) * 13;
|
|
2219
|
+
const baseTop = Math.floor(i / 4) * 22;
|
|
2220
|
+
const jX = (prng() - 0.5) * 16;
|
|
2221
|
+
const jY = (prng() - 0.5) * 10;
|
|
2222
|
+
span.style.transition = 'left 5s ease, top 5s ease';
|
|
2223
|
+
span.style.left = `${Math.max(0, Math.min(90, baseLeft + jX))}%`;
|
|
2224
|
+
span.style.top = `${Math.max(0, Math.min(90, baseTop + jY))}%`;
|
|
2225
|
+
});
|
|
2226
|
+
this._renderCanvasWatermark(this._watermarkText);
|
|
2227
|
+
}, 30000);
|
|
1721
2228
|
}
|
|
1722
2229
|
_mountWatermarkObserver() {
|
|
1723
2230
|
this._watermarkObserver?.disconnect();
|
|
1724
2231
|
this._watermarkObserver = new MutationObserver((mutations) => {
|
|
2232
|
+
let tampered = false;
|
|
1725
2233
|
for (const m of mutations) {
|
|
1726
2234
|
if (m.type === 'childList' && m.target === this.root) {
|
|
1727
2235
|
if (!this.root.contains(this.watermarkDiv)) {
|
|
1728
2236
|
this.root.appendChild(this.watermarkDiv);
|
|
2237
|
+
tampered = true;
|
|
2238
|
+
}
|
|
2239
|
+
if (!this.root.contains(this.watermarkCanvas)) {
|
|
2240
|
+
this.root.appendChild(this.watermarkCanvas);
|
|
2241
|
+
tampered = true;
|
|
1729
2242
|
}
|
|
1730
2243
|
}
|
|
1731
2244
|
if (m.type === 'attributes' && m.target === this.watermarkDiv) {
|
|
1732
2245
|
this.watermarkDiv.removeAttribute('style');
|
|
1733
2246
|
this.watermarkDiv.className = 'gvp-watermark';
|
|
1734
2247
|
this.watermarkDiv.setAttribute('aria-hidden', 'true');
|
|
2248
|
+
tampered = true;
|
|
2249
|
+
}
|
|
2250
|
+
if (m.type === 'attributes' && m.target === this.watermarkCanvas) {
|
|
2251
|
+
this.watermarkCanvas.className = 'gvp-watermark-canvas';
|
|
2252
|
+
this.watermarkCanvas.setAttribute('aria-hidden', 'true');
|
|
2253
|
+
this._renderCanvasWatermark(this._watermarkText);
|
|
2254
|
+
tampered = true;
|
|
1735
2255
|
}
|
|
1736
2256
|
if (m.type === 'childList' && m.target === this.watermarkDiv) {
|
|
1737
2257
|
if (this.watermarkDiv.childElementCount < 20) {
|
|
1738
2258
|
this._refillWatermark();
|
|
2259
|
+
tampered = true;
|
|
1739
2260
|
}
|
|
1740
2261
|
}
|
|
1741
2262
|
}
|
|
2263
|
+
if (tampered) {
|
|
2264
|
+
this.root.dispatchEvent(new CustomEvent('gv:security', {
|
|
2265
|
+
bubbles: true,
|
|
2266
|
+
detail: { type: 'watermark_tamper', timestamp: Date.now() },
|
|
2267
|
+
}));
|
|
2268
|
+
}
|
|
1742
2269
|
});
|
|
1743
2270
|
this._watermarkObserver.observe(this.root, { childList: true });
|
|
1744
2271
|
this._watermarkObserver.observe(this.watermarkDiv, { attributes: true, childList: true });
|
|
1745
2272
|
}
|
|
1746
2273
|
_refillWatermark() {
|
|
2274
|
+
const seed = this._hashCode(this._watermarkText);
|
|
2275
|
+
const prng = this._mulberry32(seed);
|
|
2276
|
+
for (let j = 0; j < this.watermarkDiv.childElementCount * 3; j++)
|
|
2277
|
+
prng();
|
|
1747
2278
|
for (let i = this.watermarkDiv.childElementCount; i < 20; i++) {
|
|
1748
2279
|
const span = el('span', 'gvp-watermark-text');
|
|
1749
2280
|
span.textContent = this._watermarkText;
|
|
1750
|
-
|
|
1751
|
-
|
|
2281
|
+
const baseLeft = (i % 4) * 26 + (Math.floor(i / 4) % 2) * 13;
|
|
2282
|
+
const baseTop = Math.floor(i / 4) * 22;
|
|
2283
|
+
const jX = (prng() - 0.5) * 16;
|
|
2284
|
+
const jY = (prng() - 0.5) * 10;
|
|
2285
|
+
span.style.left = `${Math.max(0, Math.min(90, baseLeft + jX))}%`;
|
|
2286
|
+
span.style.top = `${Math.max(0, Math.min(90, baseTop + jY))}%`;
|
|
2287
|
+
const rotJitter = -28 + (prng() - 0.5) * 8;
|
|
2288
|
+
span.style.transform = `rotate(${rotJitter}deg)`;
|
|
2289
|
+
span.style.webkitTransform = `rotate(${rotJitter}deg)`;
|
|
1752
2290
|
this.watermarkDiv.appendChild(span);
|
|
1753
2291
|
}
|
|
1754
2292
|
}
|
|
@@ -1939,6 +2477,8 @@ class PlayerUI {
|
|
|
1939
2477
|
document.removeEventListener('keydown', this._ctxKeyDownBound);
|
|
1940
2478
|
this._hideContextMenu();
|
|
1941
2479
|
this._watermarkObserver?.disconnect();
|
|
2480
|
+
if (this._watermarkDriftTimer)
|
|
2481
|
+
clearInterval(this._watermarkDriftTimer);
|
|
1942
2482
|
this.corePlayer.destroy();
|
|
1943
2483
|
this.root.remove();
|
|
1944
2484
|
}
|
|
@@ -2079,7 +2619,10 @@ function useGuardVideoPlayer(options) {
|
|
|
2079
2619
|
};
|
|
2080
2620
|
}
|
|
2081
2621
|
|
|
2622
|
+
exports.EventTracker = EventTracker;
|
|
2082
2623
|
exports.GuardVideoPlayer = GuardVideoPlayerComponent;
|
|
2083
2624
|
exports.GuardVideoPlayerCore = GuardVideoPlayer;
|
|
2625
|
+
exports.WatchChunkAccumulator = WatchChunkAccumulator;
|
|
2626
|
+
exports.collectFingerprint = collectFingerprint;
|
|
2084
2627
|
exports.useGuardVideoPlayer = useGuardVideoPlayer;
|
|
2085
2628
|
//# sourceMappingURL=index.js.map
|