@trillboards/connect 1.0.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/CHANGELOG.md +16 -0
- package/README.md +122 -0
- package/dist/index.js +2146 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2144 -0
- package/dist/index.mjs.map +1 -0
- package/dist/trillboards-connect.global.js +104 -0
- package/dist/trillboards-connect.global.js.map +1 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2146 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
5
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// src/device.js
|
|
9
|
+
var require_device = __commonJS({
|
|
10
|
+
"src/device.js"(exports$1, module) {
|
|
11
|
+
var API_BASE = "https://api.trillboards.com/v1/partner";
|
|
12
|
+
var DeviceStatus = {
|
|
13
|
+
UNREGISTERED: "unregistered",
|
|
14
|
+
REGISTERING: "registering",
|
|
15
|
+
REGISTERED: "registered",
|
|
16
|
+
ONLINE: "online",
|
|
17
|
+
OFFLINE: "offline",
|
|
18
|
+
ERROR: "error"
|
|
19
|
+
};
|
|
20
|
+
function generateFingerprint(deviceId) {
|
|
21
|
+
if (!deviceId || typeof deviceId !== "string") {
|
|
22
|
+
throw new Error("deviceId must be a non-empty string");
|
|
23
|
+
}
|
|
24
|
+
let hash = 2166136261;
|
|
25
|
+
for (let i = 0; i < deviceId.length; i++) {
|
|
26
|
+
hash ^= deviceId.charCodeAt(i);
|
|
27
|
+
hash = Math.imul(hash, 16777619);
|
|
28
|
+
}
|
|
29
|
+
const hashHex = (hash >>> 0).toString(16).padStart(8, "0");
|
|
30
|
+
let extHash = 16777619;
|
|
31
|
+
for (let i = 0; i < deviceId.length; i++) {
|
|
32
|
+
extHash ^= deviceId.charCodeAt(i) * (i + 1);
|
|
33
|
+
extHash = Math.imul(extHash, 2166136261);
|
|
34
|
+
}
|
|
35
|
+
const extHex = (extHash >>> 0).toString(16).padStart(8, "0");
|
|
36
|
+
return `SDK_${hashHex}${extHex}`;
|
|
37
|
+
}
|
|
38
|
+
function collectDeviceMetadata() {
|
|
39
|
+
const metadata = {
|
|
40
|
+
sdk_version: "1.0.0",
|
|
41
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
42
|
+
};
|
|
43
|
+
if (typeof navigator !== "undefined") {
|
|
44
|
+
metadata.user_agent = navigator.userAgent;
|
|
45
|
+
metadata.platform = navigator.platform;
|
|
46
|
+
metadata.language = navigator.language;
|
|
47
|
+
metadata.online = navigator.onLine;
|
|
48
|
+
metadata.hardware_concurrency = navigator.hardwareConcurrency;
|
|
49
|
+
}
|
|
50
|
+
if (typeof screen !== "undefined") {
|
|
51
|
+
metadata.screen_width = screen.width;
|
|
52
|
+
metadata.screen_height = screen.height;
|
|
53
|
+
metadata.pixel_ratio = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
|
|
54
|
+
metadata.color_depth = screen.colorDepth;
|
|
55
|
+
}
|
|
56
|
+
return metadata;
|
|
57
|
+
}
|
|
58
|
+
async function registerDevice(apiKey, deviceId, metadata = {}) {
|
|
59
|
+
if (!apiKey) {
|
|
60
|
+
throw new Error("apiKey is required for device registration");
|
|
61
|
+
}
|
|
62
|
+
if (!deviceId) {
|
|
63
|
+
throw new Error("deviceId is required for device registration");
|
|
64
|
+
}
|
|
65
|
+
const browserMetadata = collectDeviceMetadata();
|
|
66
|
+
const displayInfo = {};
|
|
67
|
+
if (browserMetadata.screen_width) {
|
|
68
|
+
displayInfo.width = browserMetadata.screen_width;
|
|
69
|
+
displayInfo.height = browserMetadata.screen_height;
|
|
70
|
+
}
|
|
71
|
+
const body = {
|
|
72
|
+
device_id: deviceId,
|
|
73
|
+
device_type: metadata.device_type || "digital_signage",
|
|
74
|
+
name: metadata.name || `SDK Device: ${deviceId}`,
|
|
75
|
+
display: {
|
|
76
|
+
...displayInfo,
|
|
77
|
+
...metadata.display
|
|
78
|
+
},
|
|
79
|
+
location: metadata.location || {},
|
|
80
|
+
custom_metadata: {
|
|
81
|
+
...browserMetadata,
|
|
82
|
+
...metadata.custom
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const response = await fetch(`${API_BASE}/device`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
90
|
+
"X-SDK-Version": "1.0.0"
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify(body)
|
|
93
|
+
});
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
const errorData = await response.json().catch(() => ({}));
|
|
96
|
+
const err = new Error(errorData.message || `Device registration failed: ${response.status}`);
|
|
97
|
+
err.statusCode = response.status;
|
|
98
|
+
err.response = errorData;
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
return response.json();
|
|
102
|
+
}
|
|
103
|
+
function startHeartbeat(fingerprint, intervalMs = 6e4, options = {}) {
|
|
104
|
+
if (!fingerprint) {
|
|
105
|
+
throw new Error("fingerprint is required for heartbeat");
|
|
106
|
+
}
|
|
107
|
+
const maxRetries = options.maxRetries || 10;
|
|
108
|
+
const retryBackoff = options.retryBackoff || 1.5;
|
|
109
|
+
const onSuccess = options.onSuccess || (() => {
|
|
110
|
+
});
|
|
111
|
+
const onError = options.onError || (() => {
|
|
112
|
+
});
|
|
113
|
+
let timerId = null;
|
|
114
|
+
let consecutiveFailures = 0;
|
|
115
|
+
let totalBeats = 0;
|
|
116
|
+
let lastBeatAt = null;
|
|
117
|
+
let running = false;
|
|
118
|
+
let currentInterval = intervalMs;
|
|
119
|
+
async function sendBeat() {
|
|
120
|
+
try {
|
|
121
|
+
const response = await fetch(`${API_BASE}/device/${encodeURIComponent(fingerprint)}/heartbeat`, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: {
|
|
124
|
+
"Content-Type": "application/json",
|
|
125
|
+
"X-SDK-Version": "1.0.0"
|
|
126
|
+
},
|
|
127
|
+
body: JSON.stringify({
|
|
128
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
129
|
+
total_beats: totalBeats
|
|
130
|
+
})
|
|
131
|
+
});
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
throw new Error(`Heartbeat failed: HTTP ${response.status}`);
|
|
134
|
+
}
|
|
135
|
+
const data = await response.json();
|
|
136
|
+
consecutiveFailures = 0;
|
|
137
|
+
currentInterval = intervalMs;
|
|
138
|
+
totalBeats++;
|
|
139
|
+
lastBeatAt = /* @__PURE__ */ new Date();
|
|
140
|
+
onSuccess({
|
|
141
|
+
beat: totalBeats,
|
|
142
|
+
status: data.status || "online",
|
|
143
|
+
last_seen_at: data.last_seen_at,
|
|
144
|
+
timestamp: lastBeatAt.toISOString()
|
|
145
|
+
});
|
|
146
|
+
return data;
|
|
147
|
+
} catch (err) {
|
|
148
|
+
consecutiveFailures++;
|
|
149
|
+
onError({
|
|
150
|
+
error: err.message,
|
|
151
|
+
consecutiveFailures,
|
|
152
|
+
willRetry: consecutiveFailures < maxRetries
|
|
153
|
+
});
|
|
154
|
+
if (consecutiveFailures >= maxRetries) {
|
|
155
|
+
console.error(`[Trillboards SDK] Heartbeat stopped after ${maxRetries} consecutive failures`);
|
|
156
|
+
stop();
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
currentInterval = Math.min(
|
|
160
|
+
intervalMs * Math.pow(retryBackoff, consecutiveFailures),
|
|
161
|
+
intervalMs * 5
|
|
162
|
+
// Cap at 5x the original interval
|
|
163
|
+
);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function scheduleNext() {
|
|
168
|
+
if (!running) return;
|
|
169
|
+
timerId = setTimeout(async () => {
|
|
170
|
+
await sendBeat();
|
|
171
|
+
scheduleNext();
|
|
172
|
+
}, currentInterval);
|
|
173
|
+
}
|
|
174
|
+
function start() {
|
|
175
|
+
if (running) return;
|
|
176
|
+
running = true;
|
|
177
|
+
consecutiveFailures = 0;
|
|
178
|
+
currentInterval = intervalMs;
|
|
179
|
+
sendBeat().then(() => {
|
|
180
|
+
scheduleNext();
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function stop() {
|
|
184
|
+
running = false;
|
|
185
|
+
if (timerId) {
|
|
186
|
+
clearTimeout(timerId);
|
|
187
|
+
timerId = null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function getStatus() {
|
|
191
|
+
return {
|
|
192
|
+
running,
|
|
193
|
+
totalBeats,
|
|
194
|
+
lastBeatAt: lastBeatAt ? lastBeatAt.toISOString() : null,
|
|
195
|
+
consecutiveFailures,
|
|
196
|
+
currentIntervalMs: currentInterval
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
start,
|
|
201
|
+
stop,
|
|
202
|
+
getStatus,
|
|
203
|
+
sendBeat
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
var DeviceTracker = class {
|
|
207
|
+
constructor(deviceId) {
|
|
208
|
+
this.deviceId = deviceId;
|
|
209
|
+
this.status = DeviceStatus.UNREGISTERED;
|
|
210
|
+
this.fingerprint = null;
|
|
211
|
+
this.screenId = null;
|
|
212
|
+
this.embedUrl = null;
|
|
213
|
+
this.registrationData = null;
|
|
214
|
+
this.heartbeatController = null;
|
|
215
|
+
this.listeners = {};
|
|
216
|
+
this.lastError = null;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Listen for status change events
|
|
220
|
+
* @param {string} event - Event name: 'status_change', 'registered', 'error', 'heartbeat'
|
|
221
|
+
* @param {Function} callback - Event handler
|
|
222
|
+
*/
|
|
223
|
+
on(event, callback) {
|
|
224
|
+
if (!this.listeners[event]) {
|
|
225
|
+
this.listeners[event] = [];
|
|
226
|
+
}
|
|
227
|
+
this.listeners[event].push(callback);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Remove an event listener
|
|
231
|
+
* @param {string} event - Event name
|
|
232
|
+
* @param {Function} callback - Handler to remove
|
|
233
|
+
*/
|
|
234
|
+
off(event, callback) {
|
|
235
|
+
if (this.listeners[event]) {
|
|
236
|
+
this.listeners[event] = this.listeners[event].filter((cb) => cb !== callback);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Emit an event to all registered listeners
|
|
241
|
+
* @param {string} event - Event name
|
|
242
|
+
* @param {Object} data - Event data
|
|
243
|
+
*/
|
|
244
|
+
emit(event, data) {
|
|
245
|
+
if (this.listeners[event]) {
|
|
246
|
+
this.listeners[event].forEach((cb) => {
|
|
247
|
+
try {
|
|
248
|
+
cb(data);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
console.error(`[Trillboards SDK] Event listener error for '${event}':`, err);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Update device status and emit change event
|
|
257
|
+
* @param {string} newStatus - New status from DeviceStatus enum
|
|
258
|
+
*/
|
|
259
|
+
setStatus(newStatus) {
|
|
260
|
+
const oldStatus = this.status;
|
|
261
|
+
this.status = newStatus;
|
|
262
|
+
if (oldStatus !== newStatus) {
|
|
263
|
+
this.emit("status_change", {
|
|
264
|
+
from: oldStatus,
|
|
265
|
+
to: newStatus,
|
|
266
|
+
deviceId: this.deviceId,
|
|
267
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Register the device and start heartbeat
|
|
273
|
+
* @param {string} apiKey - Partner API key
|
|
274
|
+
* @param {Object} metadata - Device metadata
|
|
275
|
+
* @param {Object} heartbeatOptions - Heartbeat configuration
|
|
276
|
+
* @returns {Promise<Object>} Registration data
|
|
277
|
+
*/
|
|
278
|
+
async register(apiKey, metadata = {}, heartbeatOptions = {}) {
|
|
279
|
+
this.setStatus(DeviceStatus.REGISTERING);
|
|
280
|
+
try {
|
|
281
|
+
const result = await registerDevice(apiKey, this.deviceId, metadata);
|
|
282
|
+
this.fingerprint = result.fingerprint;
|
|
283
|
+
this.screenId = result.screen_id;
|
|
284
|
+
this.embedUrl = result.embed_url;
|
|
285
|
+
this.registrationData = result;
|
|
286
|
+
this.lastError = null;
|
|
287
|
+
this.setStatus(DeviceStatus.REGISTERED);
|
|
288
|
+
this.emit("registered", result);
|
|
289
|
+
const interval = heartbeatOptions.interval || 6e4;
|
|
290
|
+
this.heartbeatController = startHeartbeat(this.fingerprint, interval, {
|
|
291
|
+
onSuccess: (data) => {
|
|
292
|
+
this.setStatus(DeviceStatus.ONLINE);
|
|
293
|
+
this.emit("heartbeat", data);
|
|
294
|
+
},
|
|
295
|
+
onError: (data) => {
|
|
296
|
+
if (!data.willRetry) {
|
|
297
|
+
this.setStatus(DeviceStatus.OFFLINE);
|
|
298
|
+
}
|
|
299
|
+
this.emit("heartbeat_error", data);
|
|
300
|
+
},
|
|
301
|
+
maxRetries: heartbeatOptions.maxRetries || 10,
|
|
302
|
+
retryBackoff: heartbeatOptions.retryBackoff || 1.5
|
|
303
|
+
});
|
|
304
|
+
this.heartbeatController.start();
|
|
305
|
+
return result;
|
|
306
|
+
} catch (err) {
|
|
307
|
+
this.lastError = err;
|
|
308
|
+
this.setStatus(DeviceStatus.ERROR);
|
|
309
|
+
this.emit("error", {
|
|
310
|
+
phase: "registration",
|
|
311
|
+
error: err.message,
|
|
312
|
+
statusCode: err.statusCode
|
|
313
|
+
});
|
|
314
|
+
throw err;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Stop heartbeat and clean up
|
|
319
|
+
*/
|
|
320
|
+
destroy() {
|
|
321
|
+
if (this.heartbeatController) {
|
|
322
|
+
this.heartbeatController.stop();
|
|
323
|
+
this.heartbeatController = null;
|
|
324
|
+
}
|
|
325
|
+
this.setStatus(DeviceStatus.OFFLINE);
|
|
326
|
+
this.listeners = {};
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Get the complete device state
|
|
330
|
+
* @returns {Object} Current device state
|
|
331
|
+
*/
|
|
332
|
+
getState() {
|
|
333
|
+
return {
|
|
334
|
+
deviceId: this.deviceId,
|
|
335
|
+
status: this.status,
|
|
336
|
+
fingerprint: this.fingerprint,
|
|
337
|
+
screenId: this.screenId,
|
|
338
|
+
embedUrl: this.embedUrl,
|
|
339
|
+
heartbeat: this.heartbeatController ? this.heartbeatController.getStatus() : null,
|
|
340
|
+
lastError: this.lastError ? this.lastError.message : null
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
module.exports = {
|
|
345
|
+
registerDevice,
|
|
346
|
+
generateFingerprint,
|
|
347
|
+
startHeartbeat,
|
|
348
|
+
DeviceTracker,
|
|
349
|
+
DeviceStatus,
|
|
350
|
+
collectDeviceMetadata
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// src/auction.js
|
|
356
|
+
var require_auction = __commonJS({
|
|
357
|
+
"src/auction.js"(exports$1, module) {
|
|
358
|
+
var API_BASE = "https://api.trillboards.com/v1/partner";
|
|
359
|
+
function parseVast(vastXml) {
|
|
360
|
+
if (!vastXml || typeof vastXml !== "string") {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
let doc;
|
|
364
|
+
if (typeof DOMParser !== "undefined") {
|
|
365
|
+
const parser = new DOMParser();
|
|
366
|
+
doc = parser.parseFromString(vastXml, "text/xml");
|
|
367
|
+
} else {
|
|
368
|
+
return {
|
|
369
|
+
mediaFiles: [],
|
|
370
|
+
tracking: {},
|
|
371
|
+
impressionUrls: [],
|
|
372
|
+
errorUrls: [],
|
|
373
|
+
raw: vastXml
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const result = {
|
|
377
|
+
mediaFiles: [],
|
|
378
|
+
tracking: {},
|
|
379
|
+
impressionUrls: [],
|
|
380
|
+
errorUrls: [],
|
|
381
|
+
clickThrough: null,
|
|
382
|
+
clickTracking: [],
|
|
383
|
+
duration: 0
|
|
384
|
+
};
|
|
385
|
+
const impressions = doc.querySelectorAll("Impression");
|
|
386
|
+
impressions.forEach((imp) => {
|
|
387
|
+
const url = (imp.textContent || "").trim();
|
|
388
|
+
if (url) result.impressionUrls.push(url);
|
|
389
|
+
});
|
|
390
|
+
const errors = doc.querySelectorAll("Error");
|
|
391
|
+
errors.forEach((err) => {
|
|
392
|
+
const url = (err.textContent || "").trim();
|
|
393
|
+
if (url) result.errorUrls.push(url);
|
|
394
|
+
});
|
|
395
|
+
const mediaFiles = doc.querySelectorAll("MediaFile");
|
|
396
|
+
mediaFiles.forEach((mf) => {
|
|
397
|
+
result.mediaFiles.push({
|
|
398
|
+
url: (mf.textContent || "").trim(),
|
|
399
|
+
type: mf.getAttribute("type") || "video/mp4",
|
|
400
|
+
width: parseInt(mf.getAttribute("width")) || 0,
|
|
401
|
+
height: parseInt(mf.getAttribute("height")) || 0,
|
|
402
|
+
bitrate: parseInt(mf.getAttribute("bitrate")) || 0,
|
|
403
|
+
delivery: mf.getAttribute("delivery") || "progressive"
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
const trackingEvents = doc.querySelectorAll("Tracking");
|
|
407
|
+
trackingEvents.forEach((te) => {
|
|
408
|
+
const event = te.getAttribute("event");
|
|
409
|
+
const url = (te.textContent || "").trim();
|
|
410
|
+
if (event && url) {
|
|
411
|
+
if (!result.tracking[event]) {
|
|
412
|
+
result.tracking[event] = [];
|
|
413
|
+
}
|
|
414
|
+
result.tracking[event].push(url);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
const clickThrough = doc.querySelector("ClickThrough");
|
|
418
|
+
if (clickThrough) {
|
|
419
|
+
result.clickThrough = (clickThrough.textContent || "").trim();
|
|
420
|
+
}
|
|
421
|
+
const clickTrackings = doc.querySelectorAll("ClickTracking");
|
|
422
|
+
clickTrackings.forEach((ct) => {
|
|
423
|
+
const url = (ct.textContent || "").trim();
|
|
424
|
+
if (url) result.clickTracking.push(url);
|
|
425
|
+
});
|
|
426
|
+
const duration = doc.querySelector("Duration");
|
|
427
|
+
if (duration) {
|
|
428
|
+
const timeStr = (duration.textContent || "").trim();
|
|
429
|
+
const parts = timeStr.split(":");
|
|
430
|
+
if (parts.length === 3) {
|
|
431
|
+
result.duration = parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseFloat(parts[2]);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return result;
|
|
435
|
+
}
|
|
436
|
+
function firePixel(url) {
|
|
437
|
+
return new Promise((resolve) => {
|
|
438
|
+
if (!url) {
|
|
439
|
+
resolve(false);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
443
|
+
const pixelUrl = `${url}${separator}cb=${Date.now()}`;
|
|
444
|
+
if (typeof Image !== "undefined") {
|
|
445
|
+
const img = new Image();
|
|
446
|
+
img.onload = () => resolve(true);
|
|
447
|
+
img.onerror = () => resolve(false);
|
|
448
|
+
img.src = pixelUrl;
|
|
449
|
+
setTimeout(() => resolve(false), 5e3);
|
|
450
|
+
} else {
|
|
451
|
+
fetch(pixelUrl, { method: "GET", mode: "no-cors" }).then(() => resolve(true)).catch(() => resolve(false));
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
async function fetchAds(fingerprint, options = {}) {
|
|
456
|
+
if (!fingerprint) {
|
|
457
|
+
throw new Error("fingerprint is required to fetch ads");
|
|
458
|
+
}
|
|
459
|
+
const headers = {
|
|
460
|
+
"Accept": "application/json",
|
|
461
|
+
"X-SDK-Version": "1.0.0"
|
|
462
|
+
};
|
|
463
|
+
if (options.etag) {
|
|
464
|
+
headers["If-None-Match"] = options.etag;
|
|
465
|
+
}
|
|
466
|
+
const response = await fetch(`${API_BASE}/device/${encodeURIComponent(fingerprint)}/ads`, {
|
|
467
|
+
method: "GET",
|
|
468
|
+
headers
|
|
469
|
+
});
|
|
470
|
+
if (response.status === 304) {
|
|
471
|
+
return { ads: [], unchanged: true, etag: options.etag };
|
|
472
|
+
}
|
|
473
|
+
if (!response.ok) {
|
|
474
|
+
const errorData = await response.json().catch(() => ({}));
|
|
475
|
+
const err = new Error(errorData.message || `Failed to fetch ads: ${response.status}`);
|
|
476
|
+
err.statusCode = response.status;
|
|
477
|
+
throw err;
|
|
478
|
+
}
|
|
479
|
+
const data = await response.json();
|
|
480
|
+
return {
|
|
481
|
+
ads: data.ads || [],
|
|
482
|
+
settings: data.settings || {},
|
|
483
|
+
cache_until: data.cache_until,
|
|
484
|
+
etag: data.etag || null,
|
|
485
|
+
device: data.device || {},
|
|
486
|
+
unchanged: false
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function renderAd(ad, container, options = {}) {
|
|
490
|
+
if (!ad || !ad.url) {
|
|
491
|
+
throw new Error("Valid ad object with url is required");
|
|
492
|
+
}
|
|
493
|
+
const containerEl = typeof container === "string" ? document.querySelector(container) : container;
|
|
494
|
+
if (!containerEl) {
|
|
495
|
+
throw new Error("Container element not found");
|
|
496
|
+
}
|
|
497
|
+
const muted = options.muted !== false;
|
|
498
|
+
const autoplay = options.autoplay !== false;
|
|
499
|
+
const objectFit = options.objectFit || "cover";
|
|
500
|
+
const onStart = options.onStart || (() => {
|
|
501
|
+
});
|
|
502
|
+
const onComplete = options.onComplete || (() => {
|
|
503
|
+
});
|
|
504
|
+
const onError = options.onError || (() => {
|
|
505
|
+
});
|
|
506
|
+
let element = null;
|
|
507
|
+
let completionTimer = null;
|
|
508
|
+
let startTime = null;
|
|
509
|
+
let destroyed = false;
|
|
510
|
+
containerEl.innerHTML = "";
|
|
511
|
+
containerEl.style.position = containerEl.style.position || "relative";
|
|
512
|
+
containerEl.style.overflow = "hidden";
|
|
513
|
+
if (ad.type === "video") {
|
|
514
|
+
element = document.createElement("video");
|
|
515
|
+
element.src = ad.url;
|
|
516
|
+
element.muted = muted;
|
|
517
|
+
element.autoplay = autoplay;
|
|
518
|
+
element.playsInline = true;
|
|
519
|
+
element.setAttribute("playsinline", "");
|
|
520
|
+
element.setAttribute("webkit-playsinline", "");
|
|
521
|
+
element.style.width = "100%";
|
|
522
|
+
element.style.height = "100%";
|
|
523
|
+
element.style.objectFit = objectFit;
|
|
524
|
+
element.style.display = "block";
|
|
525
|
+
element.addEventListener("play", () => {
|
|
526
|
+
if (!destroyed) {
|
|
527
|
+
startTime = Date.now();
|
|
528
|
+
onStart({ ad, type: "video", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
element.addEventListener("ended", () => {
|
|
532
|
+
if (!destroyed) {
|
|
533
|
+
onComplete({
|
|
534
|
+
ad,
|
|
535
|
+
type: "video",
|
|
536
|
+
duration: startTime ? (Date.now() - startTime) / 1e3 : 0,
|
|
537
|
+
completed: true,
|
|
538
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
element.addEventListener("error", (e) => {
|
|
543
|
+
if (!destroyed) {
|
|
544
|
+
onError({
|
|
545
|
+
ad,
|
|
546
|
+
type: "video",
|
|
547
|
+
error: e.message || "Video playback error",
|
|
548
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
containerEl.appendChild(element);
|
|
553
|
+
} else {
|
|
554
|
+
element = document.createElement("img");
|
|
555
|
+
element.src = ad.url;
|
|
556
|
+
element.alt = ad.title || "Advertisement";
|
|
557
|
+
element.style.width = "100%";
|
|
558
|
+
element.style.height = "100%";
|
|
559
|
+
element.style.objectFit = objectFit;
|
|
560
|
+
element.style.display = "block";
|
|
561
|
+
element.addEventListener("load", () => {
|
|
562
|
+
if (!destroyed) {
|
|
563
|
+
startTime = Date.now();
|
|
564
|
+
onStart({ ad, type: "image", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
565
|
+
const duration = (ad.duration || options.imageDuration || 8) * 1e3;
|
|
566
|
+
completionTimer = setTimeout(() => {
|
|
567
|
+
if (!destroyed) {
|
|
568
|
+
onComplete({
|
|
569
|
+
ad,
|
|
570
|
+
type: "image",
|
|
571
|
+
duration: ad.duration || options.imageDuration || 8,
|
|
572
|
+
completed: true,
|
|
573
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}, duration);
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
element.addEventListener("error", () => {
|
|
580
|
+
if (!destroyed) {
|
|
581
|
+
onError({
|
|
582
|
+
ad,
|
|
583
|
+
type: "image",
|
|
584
|
+
error: "Image failed to load",
|
|
585
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
containerEl.appendChild(element);
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
pause() {
|
|
593
|
+
if (element && element.pause) {
|
|
594
|
+
element.pause();
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
resume() {
|
|
598
|
+
if (element && element.play) {
|
|
599
|
+
element.play().catch(() => {
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
destroy() {
|
|
604
|
+
destroyed = true;
|
|
605
|
+
if (completionTimer) {
|
|
606
|
+
clearTimeout(completionTimer);
|
|
607
|
+
completionTimer = null;
|
|
608
|
+
}
|
|
609
|
+
if (element) {
|
|
610
|
+
if (element.pause) element.pause();
|
|
611
|
+
if (element.parentNode) element.parentNode.removeChild(element);
|
|
612
|
+
element = null;
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
getPlaybackState() {
|
|
616
|
+
if (!element) return { state: "destroyed" };
|
|
617
|
+
if (ad.type === "video") {
|
|
618
|
+
return {
|
|
619
|
+
state: element.paused ? "paused" : "playing",
|
|
620
|
+
currentTime: element.currentTime,
|
|
621
|
+
duration: element.duration,
|
|
622
|
+
muted: element.muted
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
state: startTime ? "showing" : "loading",
|
|
627
|
+
elapsed: startTime ? (Date.now() - startTime) / 1e3 : 0
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
async function trackImpression(ad, fingerprint, options = {}) {
|
|
633
|
+
if (!ad || !ad.id) {
|
|
634
|
+
throw new Error("Valid ad object with id is required");
|
|
635
|
+
}
|
|
636
|
+
const results = {
|
|
637
|
+
impression_url: false,
|
|
638
|
+
pixel_url: false,
|
|
639
|
+
vast_impressions: []
|
|
640
|
+
};
|
|
641
|
+
if (ad.impression_url) {
|
|
642
|
+
try {
|
|
643
|
+
const url = new URL(ad.impression_url);
|
|
644
|
+
if (options.duration) url.searchParams.set("duration", String(options.duration));
|
|
645
|
+
if (options.completed !== void 0) url.searchParams.set("completed", String(options.completed));
|
|
646
|
+
const response = await fetch(url.toString(), {
|
|
647
|
+
method: "GET",
|
|
648
|
+
mode: "no-cors"
|
|
649
|
+
});
|
|
650
|
+
results.impression_url = true;
|
|
651
|
+
} catch (err) {
|
|
652
|
+
results.impression_url = false;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (ad.pixel_url) {
|
|
656
|
+
results.pixel_url = await firePixel(ad.pixel_url);
|
|
657
|
+
}
|
|
658
|
+
if (ad.vast_data && ad.vast_data.impressionUrls) {
|
|
659
|
+
for (const vastUrl of ad.vast_data.impressionUrls) {
|
|
660
|
+
const fired = await firePixel(vastUrl);
|
|
661
|
+
results.vast_impressions.push({ url: vastUrl, fired });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return results;
|
|
665
|
+
}
|
|
666
|
+
function createAdRotation(config) {
|
|
667
|
+
if (!config.fingerprint) {
|
|
668
|
+
throw new Error("fingerprint is required for ad rotation");
|
|
669
|
+
}
|
|
670
|
+
const intervalSeconds = config.intervalSeconds || 60;
|
|
671
|
+
const refreshMinutes = config.refreshMinutes || 5;
|
|
672
|
+
const onAdStart = config.onAdStart || (() => {
|
|
673
|
+
});
|
|
674
|
+
const onAdComplete = config.onAdComplete || (() => {
|
|
675
|
+
});
|
|
676
|
+
const onError = config.onError || (() => {
|
|
677
|
+
});
|
|
678
|
+
const onImpression = config.onImpression || (() => {
|
|
679
|
+
});
|
|
680
|
+
let ads = [];
|
|
681
|
+
let settings = {};
|
|
682
|
+
let currentIndex = 0;
|
|
683
|
+
let currentController = null;
|
|
684
|
+
let rotationTimer = null;
|
|
685
|
+
let refreshTimer = null;
|
|
686
|
+
let running = false;
|
|
687
|
+
let etag = null;
|
|
688
|
+
let totalAdsShown = 0;
|
|
689
|
+
let totalImpressions = 0;
|
|
690
|
+
async function loadAds() {
|
|
691
|
+
try {
|
|
692
|
+
const result = await fetchAds(config.fingerprint, { etag });
|
|
693
|
+
if (!result.unchanged) {
|
|
694
|
+
ads = result.ads;
|
|
695
|
+
settings = result.settings || {};
|
|
696
|
+
etag = result.etag;
|
|
697
|
+
currentIndex = 0;
|
|
698
|
+
}
|
|
699
|
+
return ads;
|
|
700
|
+
} catch (err) {
|
|
701
|
+
onError({ phase: "fetch", error: err.message });
|
|
702
|
+
return ads;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function getNextAd() {
|
|
706
|
+
if (ads.length === 0) return null;
|
|
707
|
+
const ad = ads[currentIndex % ads.length];
|
|
708
|
+
currentIndex = (currentIndex + 1) % ads.length;
|
|
709
|
+
return ad;
|
|
710
|
+
}
|
|
711
|
+
async function showNextAd() {
|
|
712
|
+
if (!running) return;
|
|
713
|
+
if (currentController) {
|
|
714
|
+
currentController.destroy();
|
|
715
|
+
currentController = null;
|
|
716
|
+
}
|
|
717
|
+
const ad = getNextAd();
|
|
718
|
+
if (!ad) {
|
|
719
|
+
rotationTimer = setTimeout(showNextAd, intervalSeconds * 1e3);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
try {
|
|
723
|
+
currentController = renderAd(ad, config.container, {
|
|
724
|
+
muted: config.muted !== false,
|
|
725
|
+
autoplay: true,
|
|
726
|
+
objectFit: config.objectFit || "cover",
|
|
727
|
+
imageDuration: settings.image_duration || ad.duration || 8,
|
|
728
|
+
onStart: (data) => {
|
|
729
|
+
totalAdsShown++;
|
|
730
|
+
onAdStart({ ...data, totalAdsShown });
|
|
731
|
+
trackImpression(ad, config.fingerprint, {
|
|
732
|
+
duration: ad.duration,
|
|
733
|
+
completed: false
|
|
734
|
+
}).then((result) => {
|
|
735
|
+
totalImpressions++;
|
|
736
|
+
onImpression({ ad, result, totalImpressions });
|
|
737
|
+
}).catch((err) => {
|
|
738
|
+
onError({ phase: "impression", error: err.message });
|
|
739
|
+
});
|
|
740
|
+
if (config.analytics) {
|
|
741
|
+
config.analytics.track("ad_start", {
|
|
742
|
+
ad_id: ad.id,
|
|
743
|
+
ad_type: ad.type,
|
|
744
|
+
fingerprint: config.fingerprint
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
},
|
|
748
|
+
onComplete: (data) => {
|
|
749
|
+
onAdComplete({ ...data, totalAdsShown });
|
|
750
|
+
if (config.analytics) {
|
|
751
|
+
config.analytics.track("ad_complete", {
|
|
752
|
+
ad_id: ad.id,
|
|
753
|
+
ad_type: ad.type,
|
|
754
|
+
duration: data.duration,
|
|
755
|
+
fingerprint: config.fingerprint
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
const waitTime = ad.type === "video" ? 0 : 0;
|
|
759
|
+
rotationTimer = setTimeout(showNextAd, waitTime);
|
|
760
|
+
},
|
|
761
|
+
onError: (data) => {
|
|
762
|
+
onError({ ...data, phase: "render" });
|
|
763
|
+
if (config.analytics) {
|
|
764
|
+
config.analytics.track("error", {
|
|
765
|
+
ad_id: ad.id,
|
|
766
|
+
error: data.error,
|
|
767
|
+
fingerprint: config.fingerprint
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
rotationTimer = setTimeout(showNextAd, 2e3);
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
if (ad.type !== "video") {
|
|
774
|
+
const displayTime = (ad.duration || settings.image_duration || 8) * 1e3;
|
|
775
|
+
rotationTimer = setTimeout(showNextAd, displayTime);
|
|
776
|
+
}
|
|
777
|
+
} catch (err) {
|
|
778
|
+
onError({ phase: "render", error: err.message, ad });
|
|
779
|
+
rotationTimer = setTimeout(showNextAd, 5e3);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
function start() {
|
|
783
|
+
if (running) return;
|
|
784
|
+
running = true;
|
|
785
|
+
loadAds().then(() => {
|
|
786
|
+
showNextAd();
|
|
787
|
+
});
|
|
788
|
+
refreshTimer = setInterval(async () => {
|
|
789
|
+
await loadAds();
|
|
790
|
+
}, refreshMinutes * 60 * 1e3);
|
|
791
|
+
}
|
|
792
|
+
function stop() {
|
|
793
|
+
running = false;
|
|
794
|
+
if (rotationTimer) {
|
|
795
|
+
clearTimeout(rotationTimer);
|
|
796
|
+
rotationTimer = null;
|
|
797
|
+
}
|
|
798
|
+
if (refreshTimer) {
|
|
799
|
+
clearInterval(refreshTimer);
|
|
800
|
+
refreshTimer = null;
|
|
801
|
+
}
|
|
802
|
+
if (currentController) {
|
|
803
|
+
currentController.destroy();
|
|
804
|
+
currentController = null;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
function next() {
|
|
808
|
+
if (!running) return;
|
|
809
|
+
if (rotationTimer) {
|
|
810
|
+
clearTimeout(rotationTimer);
|
|
811
|
+
rotationTimer = null;
|
|
812
|
+
}
|
|
813
|
+
showNextAd();
|
|
814
|
+
}
|
|
815
|
+
function getStatus() {
|
|
816
|
+
return {
|
|
817
|
+
running,
|
|
818
|
+
totalAds: ads.length,
|
|
819
|
+
currentIndex,
|
|
820
|
+
totalAdsShown,
|
|
821
|
+
totalImpressions,
|
|
822
|
+
intervalSeconds,
|
|
823
|
+
settings,
|
|
824
|
+
etag
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
start,
|
|
829
|
+
stop,
|
|
830
|
+
next,
|
|
831
|
+
getStatus,
|
|
832
|
+
loadAds
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
module.exports = {
|
|
836
|
+
fetchAds,
|
|
837
|
+
renderAd,
|
|
838
|
+
trackImpression,
|
|
839
|
+
parseVast,
|
|
840
|
+
firePixel,
|
|
841
|
+
createAdRotation
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
// src/offline.js
|
|
847
|
+
var require_offline = __commonJS({
|
|
848
|
+
"src/offline.js"(exports$1, module) {
|
|
849
|
+
var DB_NAME = "trillboards_connect";
|
|
850
|
+
var DB_VERSION = 1;
|
|
851
|
+
var AD_STORE = "cached_ads";
|
|
852
|
+
var IMPRESSION_STORE = "queued_impressions";
|
|
853
|
+
var META_STORE = "metadata";
|
|
854
|
+
var API_BASE = "https://api.trillboards.com/v1/partner";
|
|
855
|
+
function openDatabase() {
|
|
856
|
+
return new Promise((resolve, reject) => {
|
|
857
|
+
if (typeof indexedDB === "undefined") {
|
|
858
|
+
reject(new Error("IndexedDB is not available in this environment"));
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
862
|
+
request.onupgradeneeded = (event) => {
|
|
863
|
+
const db = event.target.result;
|
|
864
|
+
if (!db.objectStoreNames.contains(AD_STORE)) {
|
|
865
|
+
const adStore = db.createObjectStore(AD_STORE, { keyPath: "id" });
|
|
866
|
+
adStore.createIndex("fingerprint", "fingerprint", { unique: false });
|
|
867
|
+
adStore.createIndex("cached_at", "cached_at", { unique: false });
|
|
868
|
+
adStore.createIndex("expires_at", "expires_at", { unique: false });
|
|
869
|
+
}
|
|
870
|
+
if (!db.objectStoreNames.contains(IMPRESSION_STORE)) {
|
|
871
|
+
const impStore = db.createObjectStore(IMPRESSION_STORE, { keyPath: "queue_id", autoIncrement: true });
|
|
872
|
+
impStore.createIndex("timestamp", "timestamp", { unique: false });
|
|
873
|
+
impStore.createIndex("synced", "synced", { unique: false });
|
|
874
|
+
}
|
|
875
|
+
if (!db.objectStoreNames.contains(META_STORE)) {
|
|
876
|
+
db.createObjectStore(META_STORE, { keyPath: "key" });
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
request.onsuccess = () => resolve(request.result);
|
|
880
|
+
request.onerror = () => reject(request.error);
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
async function cacheAds(fingerprint, ads, maxCacheSize = 10) {
|
|
884
|
+
if (!ads || !Array.isArray(ads) || ads.length === 0) return 0;
|
|
885
|
+
const now = /* @__PURE__ */ new Date();
|
|
886
|
+
const expiresAt = new Date(now.getTime() + 24 * 60 * 60 * 1e3);
|
|
887
|
+
const adsToCache = ads.slice(0, maxCacheSize).map((ad) => ({
|
|
888
|
+
...ad,
|
|
889
|
+
fingerprint,
|
|
890
|
+
cached_at: now.toISOString(),
|
|
891
|
+
expires_at: expiresAt.toISOString()
|
|
892
|
+
}));
|
|
893
|
+
const db = await openDatabase();
|
|
894
|
+
return new Promise((resolve, reject) => {
|
|
895
|
+
const tx = db.transaction(AD_STORE, "readwrite");
|
|
896
|
+
const store = tx.objectStore(AD_STORE);
|
|
897
|
+
let count = 0;
|
|
898
|
+
for (const ad of adsToCache) {
|
|
899
|
+
const req = store.put(ad);
|
|
900
|
+
req.onsuccess = () => count++;
|
|
901
|
+
}
|
|
902
|
+
tx.oncomplete = () => {
|
|
903
|
+
db.close();
|
|
904
|
+
resolve(count);
|
|
905
|
+
};
|
|
906
|
+
tx.onerror = () => {
|
|
907
|
+
db.close();
|
|
908
|
+
reject(tx.error);
|
|
909
|
+
};
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
async function getCachedAds(fingerprint) {
|
|
913
|
+
const db = await openDatabase();
|
|
914
|
+
return new Promise((resolve, reject) => {
|
|
915
|
+
const tx = db.transaction(AD_STORE, "readonly");
|
|
916
|
+
const store = tx.objectStore(AD_STORE);
|
|
917
|
+
const index = store.index("fingerprint");
|
|
918
|
+
const request = index.getAll(fingerprint);
|
|
919
|
+
request.onsuccess = () => {
|
|
920
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
921
|
+
const valid = (request.result || []).filter((ad) => ad.expires_at > now);
|
|
922
|
+
db.close();
|
|
923
|
+
resolve(valid);
|
|
924
|
+
};
|
|
925
|
+
request.onerror = () => {
|
|
926
|
+
db.close();
|
|
927
|
+
reject(request.error);
|
|
928
|
+
};
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
async function clearExpiredAds() {
|
|
932
|
+
const db = await openDatabase();
|
|
933
|
+
return new Promise((resolve, reject) => {
|
|
934
|
+
const tx = db.transaction(AD_STORE, "readwrite");
|
|
935
|
+
const store = tx.objectStore(AD_STORE);
|
|
936
|
+
const index = store.index("expires_at");
|
|
937
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
938
|
+
const range = IDBKeyRange.upperBound(now);
|
|
939
|
+
const request = index.openCursor(range);
|
|
940
|
+
let removed = 0;
|
|
941
|
+
request.onsuccess = (event) => {
|
|
942
|
+
const cursor = event.target.result;
|
|
943
|
+
if (cursor) {
|
|
944
|
+
cursor.delete();
|
|
945
|
+
removed++;
|
|
946
|
+
cursor.continue();
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
tx.oncomplete = () => {
|
|
950
|
+
db.close();
|
|
951
|
+
resolve(removed);
|
|
952
|
+
};
|
|
953
|
+
tx.onerror = () => {
|
|
954
|
+
db.close();
|
|
955
|
+
reject(tx.error);
|
|
956
|
+
};
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
async function queueImpression(impressionData) {
|
|
960
|
+
if (!impressionData || !impressionData.adid) {
|
|
961
|
+
throw new Error("impressionData with adid is required");
|
|
962
|
+
}
|
|
963
|
+
const entry = {
|
|
964
|
+
...impressionData,
|
|
965
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
966
|
+
synced: false,
|
|
967
|
+
retry_count: 0
|
|
968
|
+
};
|
|
969
|
+
const db = await openDatabase();
|
|
970
|
+
return new Promise((resolve, reject) => {
|
|
971
|
+
const tx = db.transaction(IMPRESSION_STORE, "readwrite");
|
|
972
|
+
const store = tx.objectStore(IMPRESSION_STORE);
|
|
973
|
+
const request = store.add(entry);
|
|
974
|
+
let queueId;
|
|
975
|
+
request.onsuccess = () => {
|
|
976
|
+
queueId = request.result;
|
|
977
|
+
};
|
|
978
|
+
tx.oncomplete = () => {
|
|
979
|
+
db.close();
|
|
980
|
+
resolve(queueId);
|
|
981
|
+
};
|
|
982
|
+
tx.onerror = () => {
|
|
983
|
+
db.close();
|
|
984
|
+
reject(tx.error);
|
|
985
|
+
};
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
async function getUnsyncedImpressions(limit = 100) {
|
|
989
|
+
const db = await openDatabase();
|
|
990
|
+
return new Promise((resolve, reject) => {
|
|
991
|
+
const tx = db.transaction(IMPRESSION_STORE, "readonly");
|
|
992
|
+
const store = tx.objectStore(IMPRESSION_STORE);
|
|
993
|
+
const index = store.index("synced");
|
|
994
|
+
const request = index.getAll(false, limit);
|
|
995
|
+
request.onsuccess = () => {
|
|
996
|
+
db.close();
|
|
997
|
+
resolve(request.result || []);
|
|
998
|
+
};
|
|
999
|
+
request.onerror = () => {
|
|
1000
|
+
db.close();
|
|
1001
|
+
reject(request.error);
|
|
1002
|
+
};
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
async function markSynced(queueIds) {
|
|
1006
|
+
if (!queueIds || queueIds.length === 0) return 0;
|
|
1007
|
+
const db = await openDatabase();
|
|
1008
|
+
return new Promise((resolve, reject) => {
|
|
1009
|
+
const tx = db.transaction(IMPRESSION_STORE, "readwrite");
|
|
1010
|
+
const store = tx.objectStore(IMPRESSION_STORE);
|
|
1011
|
+
let count = 0;
|
|
1012
|
+
for (const id of queueIds) {
|
|
1013
|
+
const getReq = store.get(id);
|
|
1014
|
+
getReq.onsuccess = () => {
|
|
1015
|
+
const entry = getReq.result;
|
|
1016
|
+
if (entry) {
|
|
1017
|
+
entry.synced = true;
|
|
1018
|
+
entry.synced_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1019
|
+
store.put(entry);
|
|
1020
|
+
count++;
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
tx.oncomplete = () => {
|
|
1025
|
+
db.close();
|
|
1026
|
+
resolve(count);
|
|
1027
|
+
};
|
|
1028
|
+
tx.onerror = () => {
|
|
1029
|
+
db.close();
|
|
1030
|
+
reject(tx.error);
|
|
1031
|
+
};
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
async function purgeSyncedImpressions(maxAgeHours = 72) {
|
|
1035
|
+
const cutoff = new Date(Date.now() - maxAgeHours * 60 * 60 * 1e3).toISOString();
|
|
1036
|
+
const db = await openDatabase();
|
|
1037
|
+
return new Promise((resolve, reject) => {
|
|
1038
|
+
const tx = db.transaction(IMPRESSION_STORE, "readwrite");
|
|
1039
|
+
const store = tx.objectStore(IMPRESSION_STORE);
|
|
1040
|
+
const request = store.openCursor();
|
|
1041
|
+
let removed = 0;
|
|
1042
|
+
request.onsuccess = (event) => {
|
|
1043
|
+
const cursor = event.target.result;
|
|
1044
|
+
if (cursor) {
|
|
1045
|
+
const entry = cursor.value;
|
|
1046
|
+
if (entry.synced && entry.timestamp < cutoff) {
|
|
1047
|
+
cursor.delete();
|
|
1048
|
+
removed++;
|
|
1049
|
+
}
|
|
1050
|
+
cursor.continue();
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
tx.oncomplete = () => {
|
|
1054
|
+
db.close();
|
|
1055
|
+
resolve(removed);
|
|
1056
|
+
};
|
|
1057
|
+
tx.onerror = () => {
|
|
1058
|
+
db.close();
|
|
1059
|
+
reject(tx.error);
|
|
1060
|
+
};
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
async function syncImpressions(options = {}) {
|
|
1064
|
+
const batchSize = options.batchSize || 50;
|
|
1065
|
+
const onProgress = options.onProgress || (() => {
|
|
1066
|
+
});
|
|
1067
|
+
const onError = options.onError || (() => {
|
|
1068
|
+
});
|
|
1069
|
+
const results = {
|
|
1070
|
+
total: 0,
|
|
1071
|
+
synced: 0,
|
|
1072
|
+
failed: 0,
|
|
1073
|
+
batches: 0
|
|
1074
|
+
};
|
|
1075
|
+
const unsynced = await getUnsyncedImpressions(100);
|
|
1076
|
+
results.total = unsynced.length;
|
|
1077
|
+
if (unsynced.length === 0) {
|
|
1078
|
+
return results;
|
|
1079
|
+
}
|
|
1080
|
+
for (let i = 0; i < unsynced.length; i += batchSize) {
|
|
1081
|
+
const batch = unsynced.slice(i, i + batchSize);
|
|
1082
|
+
const impressions = batch.map((entry) => ({
|
|
1083
|
+
adid: entry.adid,
|
|
1084
|
+
impid: entry.impid,
|
|
1085
|
+
did: entry.did,
|
|
1086
|
+
device_fingerprint: entry.device_fingerprint,
|
|
1087
|
+
aid: entry.aid,
|
|
1088
|
+
sid: entry.sid,
|
|
1089
|
+
duration: entry.duration,
|
|
1090
|
+
completed: entry.completed,
|
|
1091
|
+
timestamp: entry.timestamp
|
|
1092
|
+
}));
|
|
1093
|
+
try {
|
|
1094
|
+
const response = await fetch(`${API_BASE}/impressions/batch`, {
|
|
1095
|
+
method: "POST",
|
|
1096
|
+
headers: {
|
|
1097
|
+
"Content-Type": "application/json",
|
|
1098
|
+
"X-SDK-Version": "1.0.0"
|
|
1099
|
+
},
|
|
1100
|
+
body: JSON.stringify({ impressions })
|
|
1101
|
+
});
|
|
1102
|
+
if (response.ok) {
|
|
1103
|
+
const data = await response.json();
|
|
1104
|
+
const syncedIds = batch.map((entry) => entry.queue_id);
|
|
1105
|
+
await markSynced(syncedIds);
|
|
1106
|
+
results.synced += data.recorded || batch.length;
|
|
1107
|
+
results.failed += data.failed || 0;
|
|
1108
|
+
} else {
|
|
1109
|
+
results.failed += batch.length;
|
|
1110
|
+
onError({
|
|
1111
|
+
batch: results.batches,
|
|
1112
|
+
status: response.status,
|
|
1113
|
+
count: batch.length
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
} catch (err) {
|
|
1117
|
+
results.failed += batch.length;
|
|
1118
|
+
onError({
|
|
1119
|
+
batch: results.batches,
|
|
1120
|
+
error: err.message,
|
|
1121
|
+
count: batch.length
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
results.batches++;
|
|
1125
|
+
onProgress({
|
|
1126
|
+
processed: Math.min(i + batchSize, unsynced.length),
|
|
1127
|
+
total: unsynced.length,
|
|
1128
|
+
synced: results.synced,
|
|
1129
|
+
failed: results.failed
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
await purgeSyncedImpressions(72).catch(() => {
|
|
1133
|
+
});
|
|
1134
|
+
return results;
|
|
1135
|
+
}
|
|
1136
|
+
function createAutoSync(options = {}) {
|
|
1137
|
+
const syncIntervalMs = options.syncIntervalMs || 5 * 60 * 1e3;
|
|
1138
|
+
const onSyncComplete = options.onSyncComplete || (() => {
|
|
1139
|
+
});
|
|
1140
|
+
const onStatusChange = options.onStatusChange || (() => {
|
|
1141
|
+
});
|
|
1142
|
+
let intervalId = null;
|
|
1143
|
+
let running = false;
|
|
1144
|
+
let syncing = false;
|
|
1145
|
+
async function doSync() {
|
|
1146
|
+
if (syncing) return;
|
|
1147
|
+
syncing = true;
|
|
1148
|
+
try {
|
|
1149
|
+
const isOnline = typeof navigator !== "undefined" ? navigator.onLine : true;
|
|
1150
|
+
if (!isOnline) {
|
|
1151
|
+
syncing = false;
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
const result = await syncImpressions({
|
|
1155
|
+
batchSize: 50,
|
|
1156
|
+
onError: (err) => {
|
|
1157
|
+
console.warn("[Trillboards SDK] Sync batch error:", err);
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
onSyncComplete(result);
|
|
1161
|
+
} catch (err) {
|
|
1162
|
+
console.error("[Trillboards SDK] Auto-sync error:", err);
|
|
1163
|
+
} finally {
|
|
1164
|
+
syncing = false;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
function handleOnline() {
|
|
1168
|
+
onStatusChange({ online: true, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1169
|
+
doSync();
|
|
1170
|
+
}
|
|
1171
|
+
function handleOffline() {
|
|
1172
|
+
onStatusChange({ online: false, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1173
|
+
}
|
|
1174
|
+
function start() {
|
|
1175
|
+
if (running) return;
|
|
1176
|
+
running = true;
|
|
1177
|
+
if (typeof window !== "undefined") {
|
|
1178
|
+
window.addEventListener("online", handleOnline);
|
|
1179
|
+
window.addEventListener("offline", handleOffline);
|
|
1180
|
+
}
|
|
1181
|
+
intervalId = setInterval(doSync, syncIntervalMs);
|
|
1182
|
+
doSync();
|
|
1183
|
+
}
|
|
1184
|
+
function stop() {
|
|
1185
|
+
running = false;
|
|
1186
|
+
if (typeof window !== "undefined") {
|
|
1187
|
+
window.removeEventListener("online", handleOnline);
|
|
1188
|
+
window.removeEventListener("offline", handleOffline);
|
|
1189
|
+
}
|
|
1190
|
+
if (intervalId) {
|
|
1191
|
+
clearInterval(intervalId);
|
|
1192
|
+
intervalId = null;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
function getStatus() {
|
|
1196
|
+
return {
|
|
1197
|
+
running,
|
|
1198
|
+
syncing,
|
|
1199
|
+
online: typeof navigator !== "undefined" ? navigator.onLine : true
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
start,
|
|
1204
|
+
stop,
|
|
1205
|
+
sync: doSync,
|
|
1206
|
+
getStatus
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
module.exports = {
|
|
1210
|
+
// Database operations
|
|
1211
|
+
openDatabase,
|
|
1212
|
+
// Ad cache
|
|
1213
|
+
cacheAds,
|
|
1214
|
+
getCachedAds,
|
|
1215
|
+
clearExpiredAds,
|
|
1216
|
+
// Impression queue
|
|
1217
|
+
queueImpression,
|
|
1218
|
+
getUnsyncedImpressions,
|
|
1219
|
+
markSynced,
|
|
1220
|
+
purgeSyncedImpressions,
|
|
1221
|
+
syncImpressions,
|
|
1222
|
+
// Auto-sync
|
|
1223
|
+
createAutoSync
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
// src/branding.js
|
|
1229
|
+
var require_branding = __commonJS({
|
|
1230
|
+
"src/branding.js"(exports$1, module) {
|
|
1231
|
+
var DEFAULT_BRANDING_ID = "trillboards-connect-branding";
|
|
1232
|
+
var CUSTOM_CSS_ID = "trillboards-connect-custom-css";
|
|
1233
|
+
var DEFAULT_CSS = `
|
|
1234
|
+
/* Trillboards Connect SDK - Default Branding */
|
|
1235
|
+
.trillboards-container {
|
|
1236
|
+
position: relative;
|
|
1237
|
+
width: 100%;
|
|
1238
|
+
height: 100%;
|
|
1239
|
+
overflow: hidden;
|
|
1240
|
+
background-color: #000000;
|
|
1241
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
.trillboards-container video,
|
|
1245
|
+
.trillboards-container img {
|
|
1246
|
+
width: 100%;
|
|
1247
|
+
height: 100%;
|
|
1248
|
+
object-fit: cover;
|
|
1249
|
+
display: block;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
.trillboards-overlay {
|
|
1253
|
+
position: absolute;
|
|
1254
|
+
bottom: 0;
|
|
1255
|
+
left: 0;
|
|
1256
|
+
right: 0;
|
|
1257
|
+
padding: 8px 16px;
|
|
1258
|
+
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
|
1259
|
+
color: #ffffff;
|
|
1260
|
+
font-size: 12px;
|
|
1261
|
+
opacity: 0;
|
|
1262
|
+
transition: opacity 0.3s ease;
|
|
1263
|
+
pointer-events: none;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
.trillboards-container:hover .trillboards-overlay {
|
|
1267
|
+
opacity: 1;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
.trillboards-badge {
|
|
1271
|
+
position: absolute;
|
|
1272
|
+
top: 8px;
|
|
1273
|
+
right: 8px;
|
|
1274
|
+
background: rgba(0, 0, 0, 0.5);
|
|
1275
|
+
color: #ffffff;
|
|
1276
|
+
font-size: 10px;
|
|
1277
|
+
padding: 2px 6px;
|
|
1278
|
+
border-radius: 4px;
|
|
1279
|
+
opacity: 0.6;
|
|
1280
|
+
transition: opacity 0.3s ease;
|
|
1281
|
+
pointer-events: none;
|
|
1282
|
+
z-index: 10;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
.trillboards-badge:hover {
|
|
1286
|
+
opacity: 1;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
.trillboards-loading {
|
|
1290
|
+
position: absolute;
|
|
1291
|
+
top: 50%;
|
|
1292
|
+
left: 50%;
|
|
1293
|
+
transform: translate(-50%, -50%);
|
|
1294
|
+
width: 40px;
|
|
1295
|
+
height: 40px;
|
|
1296
|
+
border: 3px solid rgba(255, 255, 255, 0.2);
|
|
1297
|
+
border-top: 3px solid #ffffff;
|
|
1298
|
+
border-radius: 50%;
|
|
1299
|
+
animation: trillboards-spin 1s linear infinite;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
@keyframes trillboards-spin {
|
|
1303
|
+
0% { transform: translate(-50%, -50%) rotate(0deg); }
|
|
1304
|
+
100% { transform: translate(-50%, -50%) rotate(360deg); }
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
.trillboards-error {
|
|
1308
|
+
position: absolute;
|
|
1309
|
+
top: 50%;
|
|
1310
|
+
left: 50%;
|
|
1311
|
+
transform: translate(-50%, -50%);
|
|
1312
|
+
color: #999999;
|
|
1313
|
+
font-size: 14px;
|
|
1314
|
+
text-align: center;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
.trillboards-skip-btn {
|
|
1318
|
+
position: absolute;
|
|
1319
|
+
bottom: 16px;
|
|
1320
|
+
right: 16px;
|
|
1321
|
+
background: rgba(255, 255, 255, 0.15);
|
|
1322
|
+
color: #ffffff;
|
|
1323
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
1324
|
+
padding: 8px 16px;
|
|
1325
|
+
font-size: 12px;
|
|
1326
|
+
border-radius: 4px;
|
|
1327
|
+
cursor: pointer;
|
|
1328
|
+
transition: background 0.2s ease;
|
|
1329
|
+
z-index: 10;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
.trillboards-skip-btn:hover {
|
|
1333
|
+
background: rgba(255, 255, 255, 0.25);
|
|
1334
|
+
}
|
|
1335
|
+
`;
|
|
1336
|
+
function injectDefaultBranding() {
|
|
1337
|
+
if (typeof document === "undefined") return null;
|
|
1338
|
+
if (document.getElementById(DEFAULT_BRANDING_ID)) {
|
|
1339
|
+
return document.getElementById(DEFAULT_BRANDING_ID);
|
|
1340
|
+
}
|
|
1341
|
+
const style = document.createElement("style");
|
|
1342
|
+
style.id = DEFAULT_BRANDING_ID;
|
|
1343
|
+
style.type = "text/css";
|
|
1344
|
+
style.textContent = DEFAULT_CSS;
|
|
1345
|
+
document.head.appendChild(style);
|
|
1346
|
+
return style;
|
|
1347
|
+
}
|
|
1348
|
+
async function loadCustomCss(cssUrl, options = {}) {
|
|
1349
|
+
if (typeof document === "undefined") return null;
|
|
1350
|
+
if (!cssUrl || typeof cssUrl !== "string") {
|
|
1351
|
+
return null;
|
|
1352
|
+
}
|
|
1353
|
+
const timeout = options.timeout || 5e3;
|
|
1354
|
+
const onLoad = options.onLoad || (() => {
|
|
1355
|
+
});
|
|
1356
|
+
const onError = options.onError || (() => {
|
|
1357
|
+
});
|
|
1358
|
+
const existing = document.getElementById(CUSTOM_CSS_ID);
|
|
1359
|
+
if (existing) {
|
|
1360
|
+
existing.parentNode.removeChild(existing);
|
|
1361
|
+
}
|
|
1362
|
+
return new Promise((resolve) => {
|
|
1363
|
+
const link = document.createElement("link");
|
|
1364
|
+
link.id = CUSTOM_CSS_ID;
|
|
1365
|
+
link.rel = "stylesheet";
|
|
1366
|
+
link.type = "text/css";
|
|
1367
|
+
link.href = cssUrl;
|
|
1368
|
+
const timer = setTimeout(() => {
|
|
1369
|
+
onError({ error: "CSS load timeout", url: cssUrl });
|
|
1370
|
+
resolve(null);
|
|
1371
|
+
}, timeout);
|
|
1372
|
+
link.onload = () => {
|
|
1373
|
+
clearTimeout(timer);
|
|
1374
|
+
onLoad({ url: cssUrl });
|
|
1375
|
+
resolve(link);
|
|
1376
|
+
};
|
|
1377
|
+
link.onerror = () => {
|
|
1378
|
+
clearTimeout(timer);
|
|
1379
|
+
onError({ error: "CSS load failed", url: cssUrl });
|
|
1380
|
+
resolve(null);
|
|
1381
|
+
};
|
|
1382
|
+
document.head.appendChild(link);
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
function removeBranding() {
|
|
1386
|
+
if (typeof document === "undefined") return;
|
|
1387
|
+
const defaultStyle = document.getElementById(DEFAULT_BRANDING_ID);
|
|
1388
|
+
if (defaultStyle) defaultStyle.parentNode.removeChild(defaultStyle);
|
|
1389
|
+
const customLink = document.getElementById(CUSTOM_CSS_ID);
|
|
1390
|
+
if (customLink) customLink.parentNode.removeChild(customLink);
|
|
1391
|
+
}
|
|
1392
|
+
async function applyBranding(sdkConfig = {}) {
|
|
1393
|
+
const state = {
|
|
1394
|
+
defaultApplied: false,
|
|
1395
|
+
customApplied: false,
|
|
1396
|
+
customUrl: null
|
|
1397
|
+
};
|
|
1398
|
+
const defaultEl = injectDefaultBranding();
|
|
1399
|
+
state.defaultApplied = !!defaultEl;
|
|
1400
|
+
if (sdkConfig.custom_css_url) {
|
|
1401
|
+
state.customUrl = sdkConfig.custom_css_url;
|
|
1402
|
+
const customEl = await loadCustomCss(sdkConfig.custom_css_url);
|
|
1403
|
+
state.customApplied = !!customEl;
|
|
1404
|
+
}
|
|
1405
|
+
return state;
|
|
1406
|
+
}
|
|
1407
|
+
module.exports = {
|
|
1408
|
+
injectDefaultBranding,
|
|
1409
|
+
loadCustomCss,
|
|
1410
|
+
removeBranding,
|
|
1411
|
+
applyBranding,
|
|
1412
|
+
DEFAULT_CSS
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
// src/analytics.js
|
|
1418
|
+
var require_analytics = __commonJS({
|
|
1419
|
+
"src/analytics.js"(exports$1, module) {
|
|
1420
|
+
var API_BASE = "https://api.trillboards.com/v1/partner";
|
|
1421
|
+
var EventTypes = {
|
|
1422
|
+
AD_START: "ad_start",
|
|
1423
|
+
AD_COMPLETE: "ad_complete",
|
|
1424
|
+
AD_SKIP: "ad_skip",
|
|
1425
|
+
AD_ERROR: "ad_error",
|
|
1426
|
+
ERROR: "error",
|
|
1427
|
+
HEARTBEAT: "heartbeat",
|
|
1428
|
+
SDK_INIT: "sdk_init",
|
|
1429
|
+
SDK_ERROR: "sdk_error",
|
|
1430
|
+
DEVICE_REGISTERED: "device_registered",
|
|
1431
|
+
OFFLINE_SYNC: "offline_sync",
|
|
1432
|
+
AD_IMPRESSION: "ad_impression",
|
|
1433
|
+
VISIBILITY_CHANGE: "visibility_change"
|
|
1434
|
+
};
|
|
1435
|
+
function createAnalytics(config = {}) {
|
|
1436
|
+
const fingerprint = config.fingerprint || "unknown";
|
|
1437
|
+
const apiKey = config.apiKey || null;
|
|
1438
|
+
const flushIntervalMs = config.flushIntervalMs || 3e4;
|
|
1439
|
+
const maxBufferSize = config.maxBufferSize || 50;
|
|
1440
|
+
const onFlush = config.onFlush || (() => {
|
|
1441
|
+
});
|
|
1442
|
+
const onError = config.onError || (() => {
|
|
1443
|
+
});
|
|
1444
|
+
const enabled = config.enabled !== false;
|
|
1445
|
+
let buffer = [];
|
|
1446
|
+
let flushTimer = null;
|
|
1447
|
+
let flushing = false;
|
|
1448
|
+
let running = false;
|
|
1449
|
+
let totalTracked = 0;
|
|
1450
|
+
let totalFlushed = 0;
|
|
1451
|
+
let totalErrors = 0;
|
|
1452
|
+
let sessionId = generateSessionId();
|
|
1453
|
+
function generateSessionId() {
|
|
1454
|
+
const timestamp = Date.now().toString(36);
|
|
1455
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
1456
|
+
return `sess_${timestamp}_${random}`;
|
|
1457
|
+
}
|
|
1458
|
+
function track(eventType, data = {}) {
|
|
1459
|
+
if (!enabled || !running) return null;
|
|
1460
|
+
const event = {
|
|
1461
|
+
event: eventType,
|
|
1462
|
+
fingerprint,
|
|
1463
|
+
session_id: sessionId,
|
|
1464
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1465
|
+
data: {
|
|
1466
|
+
...data,
|
|
1467
|
+
sdk_version: "1.0.0"
|
|
1468
|
+
}
|
|
1469
|
+
};
|
|
1470
|
+
buffer.push(event);
|
|
1471
|
+
totalTracked++;
|
|
1472
|
+
if (buffer.length >= maxBufferSize) {
|
|
1473
|
+
flush();
|
|
1474
|
+
}
|
|
1475
|
+
return event;
|
|
1476
|
+
}
|
|
1477
|
+
async function flush() {
|
|
1478
|
+
if (flushing || buffer.length === 0) {
|
|
1479
|
+
return { flushed: 0, errors: 0 };
|
|
1480
|
+
}
|
|
1481
|
+
flushing = true;
|
|
1482
|
+
const eventsToSend = buffer.splice(0);
|
|
1483
|
+
try {
|
|
1484
|
+
const adEvents = eventsToSend.filter(
|
|
1485
|
+
(e) => [EventTypes.AD_START, EventTypes.AD_COMPLETE, EventTypes.AD_SKIP, EventTypes.AD_IMPRESSION].includes(e.event)
|
|
1486
|
+
);
|
|
1487
|
+
const otherEvents = eventsToSend.filter(
|
|
1488
|
+
(e) => ![EventTypes.AD_START, EventTypes.AD_COMPLETE, EventTypes.AD_SKIP, EventTypes.AD_IMPRESSION].includes(e.event)
|
|
1489
|
+
);
|
|
1490
|
+
let flushed = 0;
|
|
1491
|
+
if (adEvents.length > 0) {
|
|
1492
|
+
const impressions = adEvents.filter((e) => e.data && e.data.ad_id).map((e) => ({
|
|
1493
|
+
adid: e.data.ad_id,
|
|
1494
|
+
impid: e.data.impression_id || `sdk_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,
|
|
1495
|
+
did: e.fingerprint,
|
|
1496
|
+
device_fingerprint: e.fingerprint,
|
|
1497
|
+
duration: e.data.duration || 0,
|
|
1498
|
+
completed: e.event === EventTypes.AD_COMPLETE,
|
|
1499
|
+
timestamp: e.timestamp
|
|
1500
|
+
}));
|
|
1501
|
+
if (impressions.length > 0) {
|
|
1502
|
+
const headers = {
|
|
1503
|
+
"Content-Type": "application/json",
|
|
1504
|
+
"X-SDK-Version": "1.0.0"
|
|
1505
|
+
};
|
|
1506
|
+
if (apiKey) {
|
|
1507
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
1508
|
+
}
|
|
1509
|
+
const response = await fetch(`${API_BASE}/impressions/batch`, {
|
|
1510
|
+
method: "POST",
|
|
1511
|
+
headers,
|
|
1512
|
+
body: JSON.stringify({ impressions })
|
|
1513
|
+
});
|
|
1514
|
+
if (response.ok) {
|
|
1515
|
+
flushed += impressions.length;
|
|
1516
|
+
} else {
|
|
1517
|
+
throw new Error(`Batch send failed: ${response.status}`);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
flushed += adEvents.length - adEvents.filter((e) => e.data && e.data.ad_id).length;
|
|
1521
|
+
}
|
|
1522
|
+
flushed += otherEvents.length;
|
|
1523
|
+
totalFlushed += flushed;
|
|
1524
|
+
const result = { flushed, errors: 0, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1525
|
+
onFlush(result);
|
|
1526
|
+
return result;
|
|
1527
|
+
} catch (err) {
|
|
1528
|
+
totalErrors++;
|
|
1529
|
+
buffer.unshift(...eventsToSend);
|
|
1530
|
+
if (buffer.length > maxBufferSize * 3) {
|
|
1531
|
+
buffer = buffer.slice(-maxBufferSize * 2);
|
|
1532
|
+
}
|
|
1533
|
+
const errorResult = { flushed: 0, errors: eventsToSend.length, error: err.message };
|
|
1534
|
+
onError(errorResult);
|
|
1535
|
+
return errorResult;
|
|
1536
|
+
} finally {
|
|
1537
|
+
flushing = false;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
function start() {
|
|
1541
|
+
if (running) return;
|
|
1542
|
+
running = true;
|
|
1543
|
+
sessionId = generateSessionId();
|
|
1544
|
+
flushTimer = setInterval(() => {
|
|
1545
|
+
flush();
|
|
1546
|
+
}, flushIntervalMs);
|
|
1547
|
+
if (typeof window !== "undefined") {
|
|
1548
|
+
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
1549
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
1550
|
+
}
|
|
1551
|
+
track(EventTypes.SDK_INIT, {
|
|
1552
|
+
fingerprint,
|
|
1553
|
+
flush_interval_ms: flushIntervalMs,
|
|
1554
|
+
max_buffer_size: maxBufferSize
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
async function stop() {
|
|
1558
|
+
running = false;
|
|
1559
|
+
if (flushTimer) {
|
|
1560
|
+
clearInterval(flushTimer);
|
|
1561
|
+
flushTimer = null;
|
|
1562
|
+
}
|
|
1563
|
+
if (typeof window !== "undefined") {
|
|
1564
|
+
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
1565
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
1566
|
+
}
|
|
1567
|
+
await flush();
|
|
1568
|
+
}
|
|
1569
|
+
function handleBeforeUnload() {
|
|
1570
|
+
if (buffer.length > 0 && typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
1571
|
+
const adEvents = buffer.filter((e) => e.data && e.data.ad_id);
|
|
1572
|
+
if (adEvents.length > 0) {
|
|
1573
|
+
const impressions = adEvents.map((e) => ({
|
|
1574
|
+
adid: e.data.ad_id,
|
|
1575
|
+
impid: e.data.impression_id || `sdk_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,
|
|
1576
|
+
did: e.fingerprint,
|
|
1577
|
+
duration: e.data.duration || 0,
|
|
1578
|
+
completed: e.event === EventTypes.AD_COMPLETE,
|
|
1579
|
+
timestamp: e.timestamp
|
|
1580
|
+
}));
|
|
1581
|
+
const blob = new Blob(
|
|
1582
|
+
[JSON.stringify({ impressions })],
|
|
1583
|
+
{ type: "application/json" }
|
|
1584
|
+
);
|
|
1585
|
+
navigator.sendBeacon(`${API_BASE}/impressions/batch`, blob);
|
|
1586
|
+
}
|
|
1587
|
+
buffer = [];
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
function handleVisibilityChange() {
|
|
1591
|
+
if (document.visibilityState === "hidden") {
|
|
1592
|
+
track(EventTypes.VISIBILITY_CHANGE, { visible: false });
|
|
1593
|
+
flush();
|
|
1594
|
+
} else {
|
|
1595
|
+
track(EventTypes.VISIBILITY_CHANGE, { visible: true });
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
function getStats() {
|
|
1599
|
+
return {
|
|
1600
|
+
running,
|
|
1601
|
+
enabled,
|
|
1602
|
+
sessionId,
|
|
1603
|
+
bufferSize: buffer.length,
|
|
1604
|
+
totalTracked,
|
|
1605
|
+
totalFlushed,
|
|
1606
|
+
totalErrors,
|
|
1607
|
+
fingerprint
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
async function destroy() {
|
|
1611
|
+
await stop();
|
|
1612
|
+
buffer = [];
|
|
1613
|
+
totalTracked = 0;
|
|
1614
|
+
totalFlushed = 0;
|
|
1615
|
+
totalErrors = 0;
|
|
1616
|
+
}
|
|
1617
|
+
return {
|
|
1618
|
+
track,
|
|
1619
|
+
flush,
|
|
1620
|
+
start,
|
|
1621
|
+
stop,
|
|
1622
|
+
destroy,
|
|
1623
|
+
getStats,
|
|
1624
|
+
EventTypes
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
module.exports = {
|
|
1628
|
+
createAnalytics,
|
|
1629
|
+
EventTypes
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
// src/index.js
|
|
1635
|
+
var require_src = __commonJS({
|
|
1636
|
+
"src/index.js"(exports$1, module) {
|
|
1637
|
+
var { DeviceTracker, DeviceStatus, generateFingerprint, registerDevice, startHeartbeat } = require_device();
|
|
1638
|
+
var { fetchAds, renderAd, trackImpression, parseVast, createAdRotation } = require_auction();
|
|
1639
|
+
var { cacheAds, getCachedAds, queueImpression, syncImpressions, createAutoSync, clearExpiredAds } = require_offline();
|
|
1640
|
+
var { applyBranding, injectDefaultBranding, loadCustomCss, removeBranding } = require_branding();
|
|
1641
|
+
var { createAnalytics, EventTypes } = require_analytics();
|
|
1642
|
+
var SDK_VERSION = "1.0.0";
|
|
1643
|
+
var API_BASE = "https://api.trillboards.com/v1/partner";
|
|
1644
|
+
function validateConfig(config) {
|
|
1645
|
+
if (!config) {
|
|
1646
|
+
throw new Error("TrillboardsConnect.init() requires a configuration object");
|
|
1647
|
+
}
|
|
1648
|
+
if (!config.apiKey) {
|
|
1649
|
+
throw new Error("apiKey is required. Get your API key from https://trillboards.com/earner");
|
|
1650
|
+
}
|
|
1651
|
+
if (!config.deviceId) {
|
|
1652
|
+
throw new Error("deviceId is required. Provide a unique identifier for this device.");
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
function resolveContainer(containerId) {
|
|
1656
|
+
if (!containerId) return null;
|
|
1657
|
+
if (typeof containerId === "string") {
|
|
1658
|
+
return typeof document !== "undefined" ? document.getElementById(containerId) : null;
|
|
1659
|
+
}
|
|
1660
|
+
return containerId;
|
|
1661
|
+
}
|
|
1662
|
+
var ConnectInstance = class {
|
|
1663
|
+
constructor(config) {
|
|
1664
|
+
this.config = config;
|
|
1665
|
+
this.apiKey = config.apiKey;
|
|
1666
|
+
this.deviceId = config.deviceId;
|
|
1667
|
+
this.containerId = config.containerId;
|
|
1668
|
+
this.options = config.options || {};
|
|
1669
|
+
this.deviceTracker = null;
|
|
1670
|
+
this.adRotation = null;
|
|
1671
|
+
this.autoSync = null;
|
|
1672
|
+
this.analytics = null;
|
|
1673
|
+
this.socketConnection = null;
|
|
1674
|
+
this.initialized = false;
|
|
1675
|
+
this.destroyed = false;
|
|
1676
|
+
this.fingerprint = null;
|
|
1677
|
+
this.screenId = null;
|
|
1678
|
+
this.embedUrl = null;
|
|
1679
|
+
this.sdkConfig = null;
|
|
1680
|
+
this._listeners = {};
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Register an event listener.
|
|
1684
|
+
*
|
|
1685
|
+
* @param {string} event - Event name
|
|
1686
|
+
* @param {Function} callback - Event handler
|
|
1687
|
+
* @returns {ConnectInstance} This instance for chaining
|
|
1688
|
+
*/
|
|
1689
|
+
on(event, callback) {
|
|
1690
|
+
if (!this._listeners[event]) {
|
|
1691
|
+
this._listeners[event] = [];
|
|
1692
|
+
}
|
|
1693
|
+
this._listeners[event].push(callback);
|
|
1694
|
+
return this;
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* Remove an event listener.
|
|
1698
|
+
*
|
|
1699
|
+
* @param {string} event - Event name
|
|
1700
|
+
* @param {Function} callback - Handler to remove
|
|
1701
|
+
* @returns {ConnectInstance} This instance for chaining
|
|
1702
|
+
*/
|
|
1703
|
+
off(event, callback) {
|
|
1704
|
+
if (this._listeners[event]) {
|
|
1705
|
+
this._listeners[event] = this._listeners[event].filter((cb) => cb !== callback);
|
|
1706
|
+
}
|
|
1707
|
+
return this;
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Emit an event to all registered listeners.
|
|
1711
|
+
*
|
|
1712
|
+
* @param {string} event - Event name
|
|
1713
|
+
* @param {Object} data - Event data
|
|
1714
|
+
*/
|
|
1715
|
+
_emit(event, data) {
|
|
1716
|
+
if (this._listeners[event]) {
|
|
1717
|
+
this._listeners[event].forEach((cb) => {
|
|
1718
|
+
try {
|
|
1719
|
+
cb(data);
|
|
1720
|
+
} catch (err) {
|
|
1721
|
+
console.error(`[Trillboards SDK] Listener error for '${event}':`, err);
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Initialize the SDK: register device, start heartbeat, set up ad rotation.
|
|
1728
|
+
*
|
|
1729
|
+
* @returns {Promise<ConnectInstance>} This instance
|
|
1730
|
+
*/
|
|
1731
|
+
async initialize() {
|
|
1732
|
+
if (this.initialized) {
|
|
1733
|
+
console.warn("[Trillboards SDK] Already initialized");
|
|
1734
|
+
return this;
|
|
1735
|
+
}
|
|
1736
|
+
this._emit("initializing", { deviceId: this.deviceId, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1737
|
+
if (this.options.analytics !== false) {
|
|
1738
|
+
const tempFingerprint = generateFingerprint(this.deviceId);
|
|
1739
|
+
this.analytics = createAnalytics({
|
|
1740
|
+
fingerprint: tempFingerprint,
|
|
1741
|
+
apiKey: this.apiKey,
|
|
1742
|
+
flushIntervalMs: this.options.analyticsFlushInterval || 3e4,
|
|
1743
|
+
maxBufferSize: this.options.analyticsBufferSize || 50,
|
|
1744
|
+
onFlush: (result) => {
|
|
1745
|
+
this._emit("analytics_flush", result);
|
|
1746
|
+
},
|
|
1747
|
+
onError: (err) => {
|
|
1748
|
+
this._emit("analytics_error", err);
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
this.analytics.start();
|
|
1752
|
+
}
|
|
1753
|
+
this.deviceTracker = new DeviceTracker(this.deviceId);
|
|
1754
|
+
this.deviceTracker.on("status_change", (data) => this._emit("device_status", data));
|
|
1755
|
+
this.deviceTracker.on("registered", (data) => this._emit("device_registered", data));
|
|
1756
|
+
this.deviceTracker.on("error", (data) => this._emit("device_error", data));
|
|
1757
|
+
this.deviceTracker.on("heartbeat", (data) => this._emit("heartbeat", data));
|
|
1758
|
+
this.deviceTracker.on("heartbeat_error", (data) => this._emit("heartbeat_error", data));
|
|
1759
|
+
try {
|
|
1760
|
+
const registrationResult = await this.deviceTracker.register(this.apiKey, {
|
|
1761
|
+
device_type: this.options.deviceType || "digital_signage",
|
|
1762
|
+
name: this.options.deviceName || `SDK Device: ${this.deviceId}`,
|
|
1763
|
+
display: this.options.display,
|
|
1764
|
+
location: this.options.location,
|
|
1765
|
+
custom: {
|
|
1766
|
+
sdk_version: SDK_VERSION,
|
|
1767
|
+
...this.options.metadata || {}
|
|
1768
|
+
}
|
|
1769
|
+
}, {
|
|
1770
|
+
interval: this.options.heartbeatInterval || 6e4,
|
|
1771
|
+
maxRetries: this.options.heartbeatMaxRetries || 10,
|
|
1772
|
+
retryBackoff: 1.5
|
|
1773
|
+
});
|
|
1774
|
+
this.fingerprint = registrationResult.fingerprint;
|
|
1775
|
+
this.screenId = registrationResult.screen_id;
|
|
1776
|
+
this.embedUrl = registrationResult.embed_url;
|
|
1777
|
+
if (this.analytics) {
|
|
1778
|
+
this.analytics.destroy();
|
|
1779
|
+
this.analytics = createAnalytics({
|
|
1780
|
+
fingerprint: this.fingerprint,
|
|
1781
|
+
apiKey: this.apiKey,
|
|
1782
|
+
flushIntervalMs: this.options.analyticsFlushInterval || 3e4,
|
|
1783
|
+
maxBufferSize: this.options.analyticsBufferSize || 50,
|
|
1784
|
+
onFlush: (result) => this._emit("analytics_flush", result),
|
|
1785
|
+
onError: (err) => this._emit("analytics_error", err)
|
|
1786
|
+
});
|
|
1787
|
+
this.analytics.start();
|
|
1788
|
+
this.analytics.track(EventTypes.DEVICE_REGISTERED, {
|
|
1789
|
+
fingerprint: this.fingerprint,
|
|
1790
|
+
screen_id: this.screenId
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
} catch (err) {
|
|
1794
|
+
this._emit("error", {
|
|
1795
|
+
phase: "device_registration",
|
|
1796
|
+
error: err.message,
|
|
1797
|
+
statusCode: err.statusCode
|
|
1798
|
+
});
|
|
1799
|
+
if (this.analytics) {
|
|
1800
|
+
this.analytics.track(EventTypes.SDK_ERROR, {
|
|
1801
|
+
phase: "device_registration",
|
|
1802
|
+
error: err.message
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
throw err;
|
|
1806
|
+
}
|
|
1807
|
+
try {
|
|
1808
|
+
const infoResponse = await fetch(`${API_BASE}/info`, {
|
|
1809
|
+
headers: {
|
|
1810
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
1811
|
+
"X-SDK-Version": SDK_VERSION
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
if (infoResponse.ok) {
|
|
1815
|
+
const partnerInfo = await infoResponse.json();
|
|
1816
|
+
this.sdkConfig = partnerInfo.sdk_config || {};
|
|
1817
|
+
}
|
|
1818
|
+
} catch (err) {
|
|
1819
|
+
this.sdkConfig = {};
|
|
1820
|
+
}
|
|
1821
|
+
try {
|
|
1822
|
+
await applyBranding(this.sdkConfig);
|
|
1823
|
+
} catch (err) {
|
|
1824
|
+
console.warn("[Trillboards SDK] Branding error:", err.message);
|
|
1825
|
+
}
|
|
1826
|
+
const container = resolveContainer(this.containerId);
|
|
1827
|
+
if (container && this.fingerprint) {
|
|
1828
|
+
this.adRotation = createAdRotation({
|
|
1829
|
+
fingerprint: this.fingerprint,
|
|
1830
|
+
container,
|
|
1831
|
+
intervalSeconds: this.sdkConfig.ad_interval || this.options.adInterval || 60,
|
|
1832
|
+
refreshMinutes: this.options.adRefreshMinutes || 5,
|
|
1833
|
+
muted: this.options.muted !== false,
|
|
1834
|
+
objectFit: this.options.objectFit || "cover",
|
|
1835
|
+
analytics: this.analytics,
|
|
1836
|
+
onAdStart: (data) => {
|
|
1837
|
+
this._emit("ad_start", data);
|
|
1838
|
+
},
|
|
1839
|
+
onAdComplete: (data) => {
|
|
1840
|
+
this._emit("ad_complete", data);
|
|
1841
|
+
},
|
|
1842
|
+
onError: (data) => {
|
|
1843
|
+
this._emit("ad_error", data);
|
|
1844
|
+
},
|
|
1845
|
+
onImpression: (data) => {
|
|
1846
|
+
this._emit("impression", data);
|
|
1847
|
+
}
|
|
1848
|
+
});
|
|
1849
|
+
this.adRotation.start();
|
|
1850
|
+
}
|
|
1851
|
+
if (this.options.offlineCache !== false && typeof indexedDB !== "undefined") {
|
|
1852
|
+
this.autoSync = createAutoSync({
|
|
1853
|
+
syncIntervalMs: this.options.syncInterval || 5 * 60 * 1e3,
|
|
1854
|
+
onSyncComplete: (result) => {
|
|
1855
|
+
this._emit("sync_complete", result);
|
|
1856
|
+
if (this.analytics) {
|
|
1857
|
+
this.analytics.track(EventTypes.OFFLINE_SYNC, {
|
|
1858
|
+
synced: result.synced,
|
|
1859
|
+
failed: result.failed
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
},
|
|
1863
|
+
onStatusChange: (status) => {
|
|
1864
|
+
this._emit("connectivity_change", status);
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1867
|
+
this.autoSync.start();
|
|
1868
|
+
}
|
|
1869
|
+
this._setupSocket();
|
|
1870
|
+
this.initialized = true;
|
|
1871
|
+
this._emit("ready", {
|
|
1872
|
+
deviceId: this.deviceId,
|
|
1873
|
+
fingerprint: this.fingerprint,
|
|
1874
|
+
screenId: this.screenId,
|
|
1875
|
+
embedUrl: this.embedUrl,
|
|
1876
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1877
|
+
});
|
|
1878
|
+
return this;
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Set up Socket.io connection for real-time commands.
|
|
1882
|
+
* Connects to the API's Socket.io endpoint for live updates.
|
|
1883
|
+
*/
|
|
1884
|
+
_setupSocket() {
|
|
1885
|
+
const io = typeof window !== "undefined" && window.io ? window.io : null;
|
|
1886
|
+
if (!io || !this.fingerprint) return;
|
|
1887
|
+
try {
|
|
1888
|
+
this.socketConnection = io(API_BASE.replace("/v1/partner", ""), {
|
|
1889
|
+
transports: ["websocket", "polling"],
|
|
1890
|
+
query: {
|
|
1891
|
+
fingerprint: this.fingerprint,
|
|
1892
|
+
sdk_version: SDK_VERSION,
|
|
1893
|
+
type: "partner_sdk"
|
|
1894
|
+
},
|
|
1895
|
+
reconnection: true,
|
|
1896
|
+
reconnectionAttempts: 10,
|
|
1897
|
+
reconnectionDelay: 1e3,
|
|
1898
|
+
reconnectionDelayMax: 3e4
|
|
1899
|
+
});
|
|
1900
|
+
this.socketConnection.on("connect", () => {
|
|
1901
|
+
this._emit("socket_connected", { socketId: this.socketConnection.id });
|
|
1902
|
+
this.socketConnection.emit("join_screen", {
|
|
1903
|
+
fingerprint: this.fingerprint,
|
|
1904
|
+
screen_id: this.screenId
|
|
1905
|
+
});
|
|
1906
|
+
});
|
|
1907
|
+
this.socketConnection.on("disconnect", (reason) => {
|
|
1908
|
+
this._emit("socket_disconnected", { reason });
|
|
1909
|
+
});
|
|
1910
|
+
this.socketConnection.on("refresh_ads", () => {
|
|
1911
|
+
if (this.adRotation) {
|
|
1912
|
+
this.adRotation.loadAds().then(() => {
|
|
1913
|
+
this._emit("ads_refreshed", { source: "socket" });
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
this.socketConnection.on("update_config", (data) => {
|
|
1918
|
+
if (data && data.sdk_config) {
|
|
1919
|
+
this.sdkConfig = { ...this.sdkConfig, ...data.sdk_config };
|
|
1920
|
+
this._emit("config_updated", { config: this.sdkConfig, source: "socket" });
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
this.socketConnection.on("force_reload", () => {
|
|
1924
|
+
this._emit("force_reload", { timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1925
|
+
});
|
|
1926
|
+
this.socketConnection.on("content_update", (data) => {
|
|
1927
|
+
this._emit("content_update", data);
|
|
1928
|
+
if (this.adRotation) {
|
|
1929
|
+
this.adRotation.loadAds();
|
|
1930
|
+
}
|
|
1931
|
+
});
|
|
1932
|
+
this.socketConnection.on("connect_error", (err) => {
|
|
1933
|
+
this._emit("socket_error", { error: err.message });
|
|
1934
|
+
});
|
|
1935
|
+
} catch (err) {
|
|
1936
|
+
console.warn("[Trillboards SDK] Socket.io setup failed:", err.message);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
/**
|
|
1940
|
+
* Manually trigger the next ad in rotation.
|
|
1941
|
+
*/
|
|
1942
|
+
nextAd() {
|
|
1943
|
+
if (this.adRotation) {
|
|
1944
|
+
this.adRotation.next();
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Pause ad rotation.
|
|
1949
|
+
*/
|
|
1950
|
+
pause() {
|
|
1951
|
+
if (this.adRotation) {
|
|
1952
|
+
this.adRotation.stop();
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
/**
|
|
1956
|
+
* Resume ad rotation.
|
|
1957
|
+
*/
|
|
1958
|
+
resume() {
|
|
1959
|
+
if (this.adRotation) {
|
|
1960
|
+
this.adRotation.start();
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* Get the current state of all subsystems.
|
|
1965
|
+
*
|
|
1966
|
+
* @returns {Object} Complete SDK state
|
|
1967
|
+
*/
|
|
1968
|
+
getState() {
|
|
1969
|
+
return {
|
|
1970
|
+
version: SDK_VERSION,
|
|
1971
|
+
initialized: this.initialized,
|
|
1972
|
+
destroyed: this.destroyed,
|
|
1973
|
+
device: this.deviceTracker ? this.deviceTracker.getState() : null,
|
|
1974
|
+
adRotation: this.adRotation ? this.adRotation.getStatus() : null,
|
|
1975
|
+
offlineSync: this.autoSync ? this.autoSync.getStatus() : null,
|
|
1976
|
+
analytics: this.analytics ? this.analytics.getStats() : null,
|
|
1977
|
+
socket: this.socketConnection ? {
|
|
1978
|
+
connected: this.socketConnection.connected,
|
|
1979
|
+
id: this.socketConnection.id
|
|
1980
|
+
} : null,
|
|
1981
|
+
config: this.sdkConfig
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1985
|
+
* Queue an impression for offline sync.
|
|
1986
|
+
*
|
|
1987
|
+
* @param {Object} impressionData - Impression data
|
|
1988
|
+
* @returns {Promise<number>} Queue ID
|
|
1989
|
+
*/
|
|
1990
|
+
async queueOfflineImpression(impressionData) {
|
|
1991
|
+
return queueImpression({
|
|
1992
|
+
...impressionData,
|
|
1993
|
+
device_fingerprint: this.fingerprint
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Force sync of offline impressions.
|
|
1998
|
+
*
|
|
1999
|
+
* @returns {Promise<Object>} Sync results
|
|
2000
|
+
*/
|
|
2001
|
+
async syncOffline() {
|
|
2002
|
+
return syncImpressions({
|
|
2003
|
+
batchSize: 50,
|
|
2004
|
+
onError: (err) => {
|
|
2005
|
+
this._emit("sync_error", err);
|
|
2006
|
+
}
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Cache ads for offline use.
|
|
2011
|
+
*
|
|
2012
|
+
* @param {Array} ads - Array of ad objects
|
|
2013
|
+
* @returns {Promise<number>} Number of ads cached
|
|
2014
|
+
*/
|
|
2015
|
+
async cacheAdsOffline(ads) {
|
|
2016
|
+
if (!this.fingerprint) return 0;
|
|
2017
|
+
const maxCache = this.sdkConfig?.cache_size || this.options.cacheSize || 10;
|
|
2018
|
+
return cacheAds(this.fingerprint, ads, maxCache);
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* Destroy the SDK instance and clean up all resources.
|
|
2022
|
+
*/
|
|
2023
|
+
async destroy() {
|
|
2024
|
+
if (this.destroyed) return;
|
|
2025
|
+
this.destroyed = true;
|
|
2026
|
+
this._emit("destroying", { timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2027
|
+
if (this.adRotation) {
|
|
2028
|
+
this.adRotation.stop();
|
|
2029
|
+
this.adRotation = null;
|
|
2030
|
+
}
|
|
2031
|
+
if (this.socketConnection) {
|
|
2032
|
+
this.socketConnection.disconnect();
|
|
2033
|
+
this.socketConnection = null;
|
|
2034
|
+
}
|
|
2035
|
+
if (this.autoSync) {
|
|
2036
|
+
this.autoSync.stop();
|
|
2037
|
+
this.autoSync = null;
|
|
2038
|
+
}
|
|
2039
|
+
if (this.analytics) {
|
|
2040
|
+
await this.analytics.destroy();
|
|
2041
|
+
this.analytics = null;
|
|
2042
|
+
}
|
|
2043
|
+
if (this.deviceTracker) {
|
|
2044
|
+
this.deviceTracker.destroy();
|
|
2045
|
+
this.deviceTracker = null;
|
|
2046
|
+
}
|
|
2047
|
+
removeBranding();
|
|
2048
|
+
this.initialized = false;
|
|
2049
|
+
this._listeners = {};
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
var TrillboardsConnect = {
|
|
2053
|
+
/**
|
|
2054
|
+
* SDK version
|
|
2055
|
+
*/
|
|
2056
|
+
version: SDK_VERSION,
|
|
2057
|
+
/**
|
|
2058
|
+
* Initialize the SDK with configuration.
|
|
2059
|
+
*
|
|
2060
|
+
* @param {Object} config - SDK configuration
|
|
2061
|
+
* @param {string} config.apiKey - Partner API key (trb_partner_xxx)
|
|
2062
|
+
* @param {string} config.deviceId - Unique device identifier
|
|
2063
|
+
* @param {string|HTMLElement} config.containerId - DOM container for ad rendering
|
|
2064
|
+
* @param {Object} config.options - Additional options
|
|
2065
|
+
* @param {number} config.options.heartbeatInterval - Heartbeat interval ms (default 60000)
|
|
2066
|
+
* @param {number} config.options.adInterval - Ad rotation interval seconds (default 60)
|
|
2067
|
+
* @param {boolean} config.options.muted - Start ads muted (default true)
|
|
2068
|
+
* @param {boolean} config.options.offlineCache - Enable offline caching (default true)
|
|
2069
|
+
* @param {boolean} config.options.analytics - Enable analytics (default true)
|
|
2070
|
+
* @param {string} config.options.deviceType - Device type (default 'digital_signage')
|
|
2071
|
+
* @param {string} config.options.deviceName - Friendly device name
|
|
2072
|
+
* @param {Object} config.options.display - Display specs { width, height }
|
|
2073
|
+
* @param {Object} config.options.location - Location { lat, lng, address, venue_type }
|
|
2074
|
+
* @param {Object} config.options.metadata - Custom metadata
|
|
2075
|
+
* @returns {ConnectInstance} SDK instance (call .on('ready', ...) for initialization)
|
|
2076
|
+
*/
|
|
2077
|
+
init(config) {
|
|
2078
|
+
validateConfig(config);
|
|
2079
|
+
const instance = new ConnectInstance(config);
|
|
2080
|
+
instance.initialize().catch((err) => {
|
|
2081
|
+
console.error("[Trillboards SDK] Initialization failed:", err.message);
|
|
2082
|
+
instance._emit("error", {
|
|
2083
|
+
phase: "init",
|
|
2084
|
+
error: err.message,
|
|
2085
|
+
fatal: true
|
|
2086
|
+
});
|
|
2087
|
+
});
|
|
2088
|
+
return instance;
|
|
2089
|
+
},
|
|
2090
|
+
/**
|
|
2091
|
+
* Create an instance without auto-initialization (for advanced use cases).
|
|
2092
|
+
*
|
|
2093
|
+
* @param {Object} config - SDK configuration
|
|
2094
|
+
* @returns {ConnectInstance} Uninitialized SDK instance
|
|
2095
|
+
*/
|
|
2096
|
+
create(config) {
|
|
2097
|
+
validateConfig(config);
|
|
2098
|
+
return new ConnectInstance(config);
|
|
2099
|
+
},
|
|
2100
|
+
// Expose sub-modules for advanced usage
|
|
2101
|
+
device: {
|
|
2102
|
+
register: registerDevice,
|
|
2103
|
+
generateFingerprint,
|
|
2104
|
+
startHeartbeat,
|
|
2105
|
+
DeviceTracker,
|
|
2106
|
+
DeviceStatus
|
|
2107
|
+
},
|
|
2108
|
+
auction: {
|
|
2109
|
+
fetchAds,
|
|
2110
|
+
renderAd,
|
|
2111
|
+
trackImpression,
|
|
2112
|
+
parseVast,
|
|
2113
|
+
createAdRotation
|
|
2114
|
+
},
|
|
2115
|
+
offline: {
|
|
2116
|
+
cacheAds,
|
|
2117
|
+
getCachedAds,
|
|
2118
|
+
queueImpression,
|
|
2119
|
+
syncImpressions,
|
|
2120
|
+
createAutoSync,
|
|
2121
|
+
clearExpiredAds
|
|
2122
|
+
},
|
|
2123
|
+
branding: {
|
|
2124
|
+
applyBranding,
|
|
2125
|
+
injectDefaultBranding,
|
|
2126
|
+
loadCustomCss,
|
|
2127
|
+
removeBranding
|
|
2128
|
+
},
|
|
2129
|
+
analytics: {
|
|
2130
|
+
createAnalytics,
|
|
2131
|
+
EventTypes
|
|
2132
|
+
}
|
|
2133
|
+
};
|
|
2134
|
+
module.exports = TrillboardsConnect;
|
|
2135
|
+
module.exports.TrillboardsConnect = TrillboardsConnect;
|
|
2136
|
+
module.exports.ConnectInstance = ConnectInstance;
|
|
2137
|
+
if (typeof window !== "undefined") {
|
|
2138
|
+
window.TrillboardsConnect = TrillboardsConnect;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
});
|
|
2142
|
+
var index = require_src();
|
|
2143
|
+
|
|
2144
|
+
module.exports = index;
|
|
2145
|
+
//# sourceMappingURL=index.js.map
|
|
2146
|
+
//# sourceMappingURL=index.js.map
|