stormcloud-video-player 0.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/LICENSE +21 -0
- package/README.md +202 -0
- package/lib/index.cjs +1748 -0
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.cts +157 -0
- package/lib/index.d.ts +157 -0
- package/lib/index.js +1706 -0
- package/lib/index.js.map +1 -0
- package/package.json +38 -0
- package/src/index.ts +20 -0
- package/src/player/StormcloudVideoPlayer.ts +1124 -0
- package/src/sdk/ima.ts +369 -0
- package/src/types.ts +124 -0
- package/src/ui/StormcloudVideoPlayer.tsx +258 -0
- package/src/utils/tracking.ts +251 -0
- package/tsconfig.json +49 -0
package/src/sdk/ima.ts
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import type { ImaController } from "../types";
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
interface Window {
|
|
5
|
+
google?: any;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createImaController(video: HTMLVideoElement): ImaController {
|
|
10
|
+
let adPlaying = false;
|
|
11
|
+
const listeners = new Map<string, Set<(payload?: any) => void>>();
|
|
12
|
+
|
|
13
|
+
function emit(event: string, payload?: any): void {
|
|
14
|
+
const set = listeners.get(event);
|
|
15
|
+
if (!set) return;
|
|
16
|
+
for (const fn of Array.from(set)) {
|
|
17
|
+
try {
|
|
18
|
+
fn(payload);
|
|
19
|
+
} catch {}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function ensureImaLoaded(): Promise<void> {
|
|
24
|
+
try {
|
|
25
|
+
const frameEl = window.frameElement as HTMLIFrameElement | null;
|
|
26
|
+
const sandboxAttr = frameEl?.getAttribute?.("sandbox") || "";
|
|
27
|
+
if (sandboxAttr) {
|
|
28
|
+
const tokens = new Set(
|
|
29
|
+
sandboxAttr
|
|
30
|
+
.split(/\s+/)
|
|
31
|
+
.map((t) => t.trim())
|
|
32
|
+
.filter((t) => t.length > 0)
|
|
33
|
+
);
|
|
34
|
+
const allowsScripts = tokens.has("allow-scripts");
|
|
35
|
+
if (!allowsScripts) {
|
|
36
|
+
// eslint-disable-next-line no-console
|
|
37
|
+
console.error(
|
|
38
|
+
"StormcloudVideoPlayer: The host page is inside a sandboxed iframe without 'allow-scripts'. Google IMA cannot run ads within sandboxed frames. Remove the sandbox attribute or include 'allow-scripts allow-same-origin'."
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch {}
|
|
43
|
+
|
|
44
|
+
if (typeof window !== "undefined" && window.google?.ima)
|
|
45
|
+
return Promise.resolve();
|
|
46
|
+
const existing = document.querySelector(
|
|
47
|
+
'script[data-ima="true"]'
|
|
48
|
+
) as HTMLScriptElement | null;
|
|
49
|
+
if (existing) {
|
|
50
|
+
return new Promise((resolve) =>
|
|
51
|
+
existing.addEventListener("load", () => resolve())
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const script = document.createElement("script");
|
|
56
|
+
script.src = "https://imasdk.googleapis.com/js/sdkloader/ima3.js";
|
|
57
|
+
script.async = true;
|
|
58
|
+
script.defer = true;
|
|
59
|
+
script.setAttribute("data-ima", "true");
|
|
60
|
+
script.onload = () => resolve();
|
|
61
|
+
script.onerror = () => reject(new Error("IMA SDK load failed"));
|
|
62
|
+
document.head.appendChild(script);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let adsManager: any | undefined;
|
|
67
|
+
let adsLoader: any | undefined;
|
|
68
|
+
let adDisplayContainer: any | undefined;
|
|
69
|
+
let adContainerEl: HTMLDivElement | undefined;
|
|
70
|
+
let lastAdTagUrl: string | undefined;
|
|
71
|
+
let retryAttempts = 0;
|
|
72
|
+
const maxRetries = 2;
|
|
73
|
+
const backoffBaseMs = 500;
|
|
74
|
+
let adsLoadedPromise: Promise<void> | undefined;
|
|
75
|
+
let adsLoadedResolve: (() => void) | undefined;
|
|
76
|
+
let adsLoadedReject: ((error: Error) => void) | undefined;
|
|
77
|
+
|
|
78
|
+
function makeAdsRequest(google: any, vastTagUrl: string) {
|
|
79
|
+
const adsRequest = new google.ima.AdsRequest();
|
|
80
|
+
adsRequest.adTagUrl = vastTagUrl;
|
|
81
|
+
adsLoader.requestAds(adsRequest);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
initialize() {
|
|
86
|
+
ensureImaLoaded()
|
|
87
|
+
.then(() => {
|
|
88
|
+
const google = window.google;
|
|
89
|
+
if (!adDisplayContainer) {
|
|
90
|
+
const container = document.createElement("div");
|
|
91
|
+
container.style.position = "absolute";
|
|
92
|
+
container.style.left = "0";
|
|
93
|
+
container.style.top = "0";
|
|
94
|
+
container.style.right = "0";
|
|
95
|
+
container.style.bottom = "0";
|
|
96
|
+
container.style.pointerEvents = "none";
|
|
97
|
+
container.style.zIndex = "2";
|
|
98
|
+
video.parentElement?.appendChild(container);
|
|
99
|
+
adContainerEl = container;
|
|
100
|
+
adDisplayContainer = new google.ima.AdDisplayContainer(
|
|
101
|
+
container,
|
|
102
|
+
video
|
|
103
|
+
);
|
|
104
|
+
try {
|
|
105
|
+
adDisplayContainer.initialize?.();
|
|
106
|
+
} catch {}
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
.catch(() => {});
|
|
110
|
+
},
|
|
111
|
+
async requestAds(vastTagUrl: string) {
|
|
112
|
+
console.log("[IMA] Requesting ads:", vastTagUrl);
|
|
113
|
+
|
|
114
|
+
adsLoadedPromise = new Promise<void>((resolve, reject) => {
|
|
115
|
+
adsLoadedResolve = resolve;
|
|
116
|
+
adsLoadedReject = reject;
|
|
117
|
+
|
|
118
|
+
setTimeout(() => {
|
|
119
|
+
if (adsLoadedReject) {
|
|
120
|
+
adsLoadedReject(new Error("Ad request timeout"));
|
|
121
|
+
adsLoadedReject = undefined;
|
|
122
|
+
adsLoadedResolve = undefined;
|
|
123
|
+
}
|
|
124
|
+
}, 10000);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
await ensureImaLoaded();
|
|
129
|
+
const google = window.google;
|
|
130
|
+
lastAdTagUrl = vastTagUrl;
|
|
131
|
+
retryAttempts = 0;
|
|
132
|
+
|
|
133
|
+
if (!adDisplayContainer) {
|
|
134
|
+
console.log("[IMA] Creating ad display container");
|
|
135
|
+
const container = document.createElement("div");
|
|
136
|
+
container.style.position = "absolute";
|
|
137
|
+
container.style.left = "0";
|
|
138
|
+
container.style.top = "0";
|
|
139
|
+
container.style.right = "0";
|
|
140
|
+
container.style.bottom = "0";
|
|
141
|
+
container.style.pointerEvents = "none";
|
|
142
|
+
container.style.zIndex = "2";
|
|
143
|
+
|
|
144
|
+
if (!video.parentElement) {
|
|
145
|
+
throw new Error("Video element has no parent for ad container");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
video.parentElement.appendChild(container);
|
|
149
|
+
adContainerEl = container;
|
|
150
|
+
adDisplayContainer = new google.ima.AdDisplayContainer(
|
|
151
|
+
container,
|
|
152
|
+
video
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
adDisplayContainer.initialize();
|
|
157
|
+
console.log("[IMA] Ad display container initialized");
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.warn(
|
|
160
|
+
"[IMA] Failed to initialize ad display container:",
|
|
161
|
+
error
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!adsLoader) {
|
|
167
|
+
console.log("[IMA] Creating ads loader");
|
|
168
|
+
const adsLoaderCls = new google.ima.AdsLoader(adDisplayContainer);
|
|
169
|
+
adsLoader = adsLoaderCls;
|
|
170
|
+
|
|
171
|
+
adsLoader.addEventListener(
|
|
172
|
+
google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
|
|
173
|
+
(evt: any) => {
|
|
174
|
+
console.log("[IMA] Ads manager loaded");
|
|
175
|
+
try {
|
|
176
|
+
adsManager = evt.getAdsManager(video);
|
|
177
|
+
const AdEvent = google.ima.AdEvent.Type;
|
|
178
|
+
const AdErrorEvent = google.ima.AdErrorEvent.Type;
|
|
179
|
+
|
|
180
|
+
adsManager.addEventListener(
|
|
181
|
+
AdErrorEvent.AD_ERROR,
|
|
182
|
+
(errorEvent: any) => {
|
|
183
|
+
console.error("[IMA] Ad error:", errorEvent.getError());
|
|
184
|
+
try {
|
|
185
|
+
adsManager?.destroy?.();
|
|
186
|
+
} catch {}
|
|
187
|
+
adPlaying = false;
|
|
188
|
+
if (adContainerEl)
|
|
189
|
+
adContainerEl.style.pointerEvents = "none";
|
|
190
|
+
|
|
191
|
+
if (adsLoadedReject) {
|
|
192
|
+
adsLoadedReject(new Error("Ad playback error"));
|
|
193
|
+
adsLoadedReject = undefined;
|
|
194
|
+
adsLoadedResolve = undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (lastAdTagUrl && retryAttempts < maxRetries) {
|
|
198
|
+
const delay =
|
|
199
|
+
backoffBaseMs * Math.pow(2, retryAttempts++);
|
|
200
|
+
console.log(
|
|
201
|
+
`[IMA] Retrying ad request in ${delay}ms (attempt ${retryAttempts})`
|
|
202
|
+
);
|
|
203
|
+
window.setTimeout(() => {
|
|
204
|
+
try {
|
|
205
|
+
makeAdsRequest(google, lastAdTagUrl!);
|
|
206
|
+
} catch {}
|
|
207
|
+
}, delay);
|
|
208
|
+
} else {
|
|
209
|
+
console.log(
|
|
210
|
+
"[IMA] Max retries reached, emitting ad_error"
|
|
211
|
+
);
|
|
212
|
+
emit("ad_error");
|
|
213
|
+
video.play().catch(() => {});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
adsManager.addEventListener(
|
|
219
|
+
AdEvent.CONTENT_PAUSE_REQUESTED,
|
|
220
|
+
() => {
|
|
221
|
+
console.log("[IMA] Content pause requested");
|
|
222
|
+
video.pause();
|
|
223
|
+
adPlaying = true;
|
|
224
|
+
if (adContainerEl)
|
|
225
|
+
adContainerEl.style.pointerEvents = "auto";
|
|
226
|
+
emit("content_pause");
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
adsManager.addEventListener(
|
|
231
|
+
AdEvent.CONTENT_RESUME_REQUESTED,
|
|
232
|
+
() => {
|
|
233
|
+
console.log("[IMA] Content resume requested");
|
|
234
|
+
adPlaying = false;
|
|
235
|
+
if (adContainerEl)
|
|
236
|
+
adContainerEl.style.pointerEvents = "none";
|
|
237
|
+
video.play().catch(() => {});
|
|
238
|
+
emit("content_resume");
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
adsManager.addEventListener(AdEvent.ALL_ADS_COMPLETED, () => {
|
|
243
|
+
console.log("[IMA] All ads completed");
|
|
244
|
+
adPlaying = false;
|
|
245
|
+
if (adContainerEl) adContainerEl.style.pointerEvents = "none";
|
|
246
|
+
video.play().catch(() => {});
|
|
247
|
+
emit("all_ads_completed");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
console.log("[IMA] Ads manager event listeners attached");
|
|
251
|
+
|
|
252
|
+
if (adsLoadedResolve) {
|
|
253
|
+
adsLoadedResolve();
|
|
254
|
+
adsLoadedResolve = undefined;
|
|
255
|
+
adsLoadedReject = undefined;
|
|
256
|
+
}
|
|
257
|
+
} catch (e) {
|
|
258
|
+
console.error("[IMA] Error setting up ads manager:", e);
|
|
259
|
+
adPlaying = false;
|
|
260
|
+
if (adContainerEl) adContainerEl.style.pointerEvents = "none";
|
|
261
|
+
video.play().catch(() => {});
|
|
262
|
+
|
|
263
|
+
if (adsLoadedReject) {
|
|
264
|
+
adsLoadedReject(new Error("Failed to setup ads manager"));
|
|
265
|
+
adsLoadedReject = undefined;
|
|
266
|
+
adsLoadedResolve = undefined;
|
|
267
|
+
}
|
|
268
|
+
emit("ad_error");
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
false
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
adsLoader.addEventListener(
|
|
275
|
+
google.ima.AdErrorEvent.Type.AD_ERROR,
|
|
276
|
+
(adErrorEvent: any) => {
|
|
277
|
+
console.error("[IMA] Ads loader error:", adErrorEvent.getError());
|
|
278
|
+
|
|
279
|
+
if (adsLoadedReject) {
|
|
280
|
+
adsLoadedReject(new Error("Ads loader error"));
|
|
281
|
+
adsLoadedReject = undefined;
|
|
282
|
+
adsLoadedResolve = undefined;
|
|
283
|
+
}
|
|
284
|
+
emit("ad_error");
|
|
285
|
+
},
|
|
286
|
+
false
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
console.log("[IMA] Making ads request");
|
|
291
|
+
makeAdsRequest(google, vastTagUrl);
|
|
292
|
+
|
|
293
|
+
return adsLoadedPromise;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error("[IMA] Failed to request ads:", error);
|
|
296
|
+
if (adsLoadedReject) {
|
|
297
|
+
adsLoadedReject(error as Error);
|
|
298
|
+
adsLoadedReject = undefined;
|
|
299
|
+
adsLoadedResolve = undefined;
|
|
300
|
+
}
|
|
301
|
+
return Promise.reject(error);
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
async play() {
|
|
305
|
+
if (!window.google?.ima || !adDisplayContainer) {
|
|
306
|
+
console.warn(
|
|
307
|
+
"[IMA] Cannot play ad: IMA SDK or ad container not available"
|
|
308
|
+
);
|
|
309
|
+
return Promise.reject(new Error("IMA SDK not available"));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!adsManager) {
|
|
313
|
+
console.warn("[IMA] Cannot play ad: No ads manager available");
|
|
314
|
+
return Promise.reject(new Error("No ads manager"));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
const width = video.clientWidth || 640;
|
|
319
|
+
const height = video.clientHeight || 360;
|
|
320
|
+
|
|
321
|
+
console.log(`[IMA] Initializing ads manager (${width}x${height})`);
|
|
322
|
+
adsManager.init(width, height, window.google.ima.ViewMode.NORMAL);
|
|
323
|
+
|
|
324
|
+
console.log("[IMA] Pausing video for ad playback");
|
|
325
|
+
video.pause();
|
|
326
|
+
adPlaying = true;
|
|
327
|
+
|
|
328
|
+
console.log("[IMA] Starting ad playback");
|
|
329
|
+
adsManager.start();
|
|
330
|
+
|
|
331
|
+
return Promise.resolve();
|
|
332
|
+
} catch (error) {
|
|
333
|
+
console.error("[IMA] Error starting ad playback:", error);
|
|
334
|
+
adPlaying = false;
|
|
335
|
+
video.play().catch(() => {});
|
|
336
|
+
return Promise.reject(error);
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
async stop() {
|
|
340
|
+
adPlaying = false;
|
|
341
|
+
try {
|
|
342
|
+
adsManager?.stop?.();
|
|
343
|
+
} catch {}
|
|
344
|
+
video.play().catch(() => {});
|
|
345
|
+
},
|
|
346
|
+
destroy() {
|
|
347
|
+
try {
|
|
348
|
+
adsManager?.destroy?.();
|
|
349
|
+
} catch {}
|
|
350
|
+
adPlaying = false;
|
|
351
|
+
try {
|
|
352
|
+
adsLoader?.destroy?.();
|
|
353
|
+
} catch {}
|
|
354
|
+
if (adContainerEl?.parentElement) {
|
|
355
|
+
adContainerEl.parentElement.removeChild(adContainerEl);
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
isAdPlaying() {
|
|
359
|
+
return adPlaying;
|
|
360
|
+
},
|
|
361
|
+
on(event: string, listener: (payload?: any) => void) {
|
|
362
|
+
if (!listeners.has(event)) listeners.set(event, new Set());
|
|
363
|
+
listeners.get(event)!.add(listener);
|
|
364
|
+
},
|
|
365
|
+
off(event: string, listener: (payload?: any) => void) {
|
|
366
|
+
listeners.get(event)?.delete(listener);
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
export type LateJoinPolicy = "play_remaining" | "skip_to_content";
|
|
2
|
+
|
|
3
|
+
export interface AdBreak {
|
|
4
|
+
id?: string;
|
|
5
|
+
startTimeMs: number;
|
|
6
|
+
durationMs?: number;
|
|
7
|
+
vastTagUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface AdSchedule {
|
|
11
|
+
breaks: AdBreak[];
|
|
12
|
+
lateJoinPolicy?: LateJoinPolicy;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface StormcloudVideoPlayerConfig {
|
|
16
|
+
videoElement: HTMLVideoElement;
|
|
17
|
+
src: string;
|
|
18
|
+
autoplay?: boolean;
|
|
19
|
+
muted?: boolean;
|
|
20
|
+
allowNativeHls?: boolean;
|
|
21
|
+
adSchedule?: AdSchedule;
|
|
22
|
+
lowLatencyMode?: boolean;
|
|
23
|
+
driftToleranceMs?: number;
|
|
24
|
+
immediateManifestAds?: boolean;
|
|
25
|
+
debugAdTiming?: boolean;
|
|
26
|
+
adFailsafeTimeoutMs?: number;
|
|
27
|
+
showCustomControls?: boolean;
|
|
28
|
+
onVolumeToggle?: () => void;
|
|
29
|
+
onFullscreenToggle?: () => void;
|
|
30
|
+
onControlClick?: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Scte35Marker {
|
|
34
|
+
type: "start" | "end" | "progress";
|
|
35
|
+
ptsSeconds?: number;
|
|
36
|
+
durationSeconds?: number;
|
|
37
|
+
raw?: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Id3TagInfo {
|
|
41
|
+
key: string;
|
|
42
|
+
value: string | Uint8Array;
|
|
43
|
+
ptsSeconds?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ImaController {
|
|
47
|
+
initialize: () => void;
|
|
48
|
+
requestAds: (vastTagUrl: string) => Promise<void>;
|
|
49
|
+
play: () => Promise<void>;
|
|
50
|
+
stop: () => Promise<void>;
|
|
51
|
+
destroy: () => void;
|
|
52
|
+
isAdPlaying: () => boolean;
|
|
53
|
+
on: (event: string, listener: (payload?: any) => void) => void;
|
|
54
|
+
off: (event: string, listener: (payload?: any) => void) => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ImaControllerOptions {
|
|
58
|
+
maxRetries?: number;
|
|
59
|
+
backoffBaseMs?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface StormcloudApiResponse {
|
|
63
|
+
response: {
|
|
64
|
+
ima: {
|
|
65
|
+
"publisherdesk.ima": {
|
|
66
|
+
payload: string;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
options: {
|
|
70
|
+
vast: {
|
|
71
|
+
cue_tones?: {
|
|
72
|
+
number_ads: number;
|
|
73
|
+
};
|
|
74
|
+
timer_vod?: {
|
|
75
|
+
interval: number;
|
|
76
|
+
number_ads: number;
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ClientInfo {
|
|
84
|
+
brand: string;
|
|
85
|
+
os: string;
|
|
86
|
+
model: string;
|
|
87
|
+
deviceType: "tv" | "mobile" | "tablet" | "desktop";
|
|
88
|
+
isSmartTV: boolean;
|
|
89
|
+
isAndroid: boolean;
|
|
90
|
+
isWebView: boolean;
|
|
91
|
+
isWebApp: boolean;
|
|
92
|
+
domain: string;
|
|
93
|
+
origin: string;
|
|
94
|
+
path: string;
|
|
95
|
+
userAgent: string;
|
|
96
|
+
vendor: string;
|
|
97
|
+
platform: string;
|
|
98
|
+
screen: {
|
|
99
|
+
width?: number;
|
|
100
|
+
height?: number;
|
|
101
|
+
availWidth?: number;
|
|
102
|
+
availHeight?: number;
|
|
103
|
+
orientation?: string;
|
|
104
|
+
pixelDepth?: number;
|
|
105
|
+
};
|
|
106
|
+
hardwareConcurrency: number;
|
|
107
|
+
deviceMemory: number | null;
|
|
108
|
+
maxTouchPoints: number;
|
|
109
|
+
language: string;
|
|
110
|
+
languages: string;
|
|
111
|
+
cookieEnabled: boolean;
|
|
112
|
+
doNotTrack: string;
|
|
113
|
+
referrer: string;
|
|
114
|
+
visibilityState: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface TrackingData extends ClientInfo {
|
|
118
|
+
browserId: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface HeartbeatData {
|
|
122
|
+
browserId: string;
|
|
123
|
+
timestamp: string;
|
|
124
|
+
}
|