@trillboards/ads-sdk 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.mjs CHANGED
@@ -2,6 +2,9 @@ import { createContext, useState, useEffect, useMemo, useContext, useCallback, u
2
2
  import { jsx } from 'react/jsx-runtime';
3
3
 
4
4
  // src/react/TrillboardsProvider.tsx
5
+
6
+ // src/core/config.ts
7
+ var SDK_VERSION = "2.2.0";
5
8
  var DEFAULT_CONFIG = {
6
9
  API_BASE: "https://api.trillboards.com/v1/partner",
7
10
  CDN_BASE: "https://cdn.trillboards.com",
@@ -56,6 +59,7 @@ var DEFAULT_PUBLIC_STATE = {
56
59
  programmaticPlaying: false,
57
60
  prefetchedReady: false,
58
61
  waterfallMode: "programmatic_only",
62
+ adDeliveryProfileMode: null,
59
63
  screenId: null,
60
64
  deviceId: null
61
65
  };
@@ -83,6 +87,7 @@ function createInitialState() {
83
87
  programmaticRetryActive: false,
84
88
  programmaticRetryCount: 0,
85
89
  programmaticLastError: null,
90
+ adDeliveryProfile: null,
86
91
  initialized: false,
87
92
  screenOrientation: null,
88
93
  screenDimensions: null,
@@ -100,6 +105,7 @@ function getPublicState(internal) {
100
105
  programmaticPlaying: internal.programmaticPlaying,
101
106
  prefetchedReady: internal.prefetchedReady ?? false,
102
107
  waterfallMode: internal.waterfallMode,
108
+ adDeliveryProfileMode: internal.adDeliveryProfile?.mode ?? null,
103
109
  screenId: internal.screenId,
104
110
  deviceId: internal.deviceId
105
111
  };
@@ -225,6 +231,9 @@ var Logger = class {
225
231
  }
226
232
  };
227
233
  var logger = new Logger();
234
+ function setLogLevel(level) {
235
+ logger.setLevel(level);
236
+ }
228
237
 
229
238
  // src/api/client.ts
230
239
  function getPlayerTruth(container) {
@@ -357,10 +366,9 @@ var ApiClient = class {
357
366
  }
358
367
  }
359
368
  /**
360
- * Ping the heartbeat endpoint to signal this device is alive.
361
- * Returns `true` on 2xx, `false` on any error.
369
+ * Ping heartbeat endpoint and return delivery profile + queued commands.
362
370
  */
363
- async sendHeartbeat(deviceId, screenId) {
371
+ async sendHeartbeat(deviceId, screenId, payload = {}) {
364
372
  logger.debug("Sending heartbeat", { deviceId });
365
373
  try {
366
374
  const response = await fetch(`${this.apiBase}/device/${deviceId}/heartbeat`, {
@@ -370,13 +378,24 @@ var ApiClient = class {
370
378
  device_id: deviceId,
371
379
  screen_id: screenId,
372
380
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
373
- status: "active"
381
+ status: "active",
382
+ ...payload
374
383
  }),
375
384
  signal: AbortSignal.timeout(5e3)
376
385
  });
377
- return response.ok;
386
+ if (!response.ok) {
387
+ return { ok: false, data: null };
388
+ }
389
+ let data = null;
390
+ try {
391
+ const json = await response.json();
392
+ data = json?.data ?? null;
393
+ } catch {
394
+ data = null;
395
+ }
396
+ return { ok: true, data };
378
397
  } catch {
379
- return false;
398
+ return { ok: false, data: null };
380
399
  }
381
400
  }
382
401
  /**
@@ -1393,6 +1412,9 @@ var _TrillboardsAds = class _TrillboardsAds {
1393
1412
  this.offlineHandler = null;
1394
1413
  this.visibilityHandler = null;
1395
1414
  this.config = resolveConfig(config);
1415
+ if (this.config.debug) {
1416
+ setLogLevel("debug");
1417
+ }
1396
1418
  this.state = createInitialState();
1397
1419
  this.events = new EventEmitter();
1398
1420
  this.api = new ApiClient(this.config.apiBase);
@@ -1666,11 +1688,104 @@ var _TrillboardsAds = class _TrillboardsAds {
1666
1688
  }
1667
1689
  startHeartbeatTimer() {
1668
1690
  if (this.state.heartbeatTimer) clearInterval(this.state.heartbeatTimer);
1691
+ const tick = () => {
1692
+ this.sendHeartbeatTick().catch(() => {
1693
+ });
1694
+ };
1695
+ tick();
1669
1696
  this.state.heartbeatTimer = setInterval(() => {
1670
- this.api.sendHeartbeat(this.config.deviceId, this.state.screenId);
1697
+ tick();
1671
1698
  }, this.config.heartbeatInterval);
1672
1699
  }
1700
+ async sendHeartbeatTick() {
1701
+ const result = await this.api.sendHeartbeat(
1702
+ this.config.deviceId,
1703
+ this.state.screenId,
1704
+ this.buildHeartbeatPayload()
1705
+ );
1706
+ if (!result.ok || !result.data) return;
1707
+ if (result.data.ad_delivery_profile?.mode) {
1708
+ this.applyAdDeliveryProfile(result.data.ad_delivery_profile);
1709
+ }
1710
+ if (Array.isArray(result.data.commands) && result.data.commands.length > 0) {
1711
+ this.runHeartbeatCommands(result.data.commands);
1712
+ }
1713
+ }
1714
+ buildHeartbeatPayload() {
1715
+ const userAgent = typeof navigator !== "undefined" ? navigator.userAgent : "";
1716
+ const platform = typeof navigator !== "undefined" ? navigator.platform : null;
1717
+ const connection = typeof navigator !== "undefined" ? navigator.connection : null;
1718
+ const chromiumMatch = userAgent.match(/(?:Chrome|Chromium)\/(\d+)/i);
1719
+ const chromiumMajor = chromiumMatch ? Number(chromiumMatch[1]) : void 0;
1720
+ const imaSupported = typeof window !== "undefined" ? Boolean(window?.google?.ima) : null;
1721
+ const telemetry = {
1722
+ powerState: typeof document !== "undefined" && document.hidden ? "standby" : "on",
1723
+ agentStatus: typeof document !== "undefined" && document.hidden ? "background" : "foreground",
1724
+ os: platform || void 0,
1725
+ agentVersion: SDK_VERSION,
1726
+ ima_integration: "html5_webview",
1727
+ ima_supported: imaSupported,
1728
+ webview_chromium_major: Number.isFinite(chromiumMajor) ? chromiumMajor : void 0
1729
+ };
1730
+ const payload = {
1731
+ telemetry,
1732
+ sdk: {
1733
+ version: SDK_VERSION,
1734
+ ima_supported: imaSupported,
1735
+ ima_integration: "html5_webview",
1736
+ webview_chromium_major: Number.isFinite(chromiumMajor) ? chromiumMajor : void 0
1737
+ },
1738
+ device: {
1739
+ os: platform || void 0,
1740
+ model: userAgent || void 0
1741
+ }
1742
+ };
1743
+ if (connection) {
1744
+ payload.network = {
1745
+ connectionType: connection.type || void 0,
1746
+ effectiveType: connection.effectiveType || void 0,
1747
+ downlinkMbps: typeof connection.downlink === "number" ? connection.downlink : void 0,
1748
+ bandwidthMbps: typeof connection.downlink === "number" ? connection.downlink : void 0,
1749
+ rtt: typeof connection.rtt === "number" ? connection.rtt : void 0,
1750
+ latencyRtt: typeof connection.rtt === "number" ? connection.rtt : void 0,
1751
+ saveData: typeof connection.saveData === "boolean" ? connection.saveData : void 0
1752
+ };
1753
+ }
1754
+ return payload;
1755
+ }
1756
+ applyAdDeliveryProfile(profile) {
1757
+ if (!profile?.mode) return;
1758
+ const previousMode = this.state.adDeliveryProfile?.mode ?? null;
1759
+ this.state.adDeliveryProfile = profile;
1760
+ if (profile.mode === "vast_fallback" && this.state.programmaticPlaying) {
1761
+ this.programmaticPlayer.stop({ silent: true });
1762
+ this.state.programmaticPlaying = false;
1763
+ }
1764
+ if (previousMode !== profile.mode) {
1765
+ this.emitStateChanged();
1766
+ }
1767
+ }
1768
+ runHeartbeatCommands(commands) {
1769
+ for (const command of commands) {
1770
+ const name = String(command?.command || "").toLowerCase();
1771
+ if (name === "refresh_ads") {
1772
+ this.refresh().catch(() => {
1773
+ });
1774
+ } else if (name === "restart") {
1775
+ if (typeof window !== "undefined" && typeof window.location?.reload === "function") {
1776
+ window.location.reload();
1777
+ return;
1778
+ }
1779
+ this.refresh().catch(() => {
1780
+ });
1781
+ }
1782
+ }
1783
+ }
1673
1784
  playNextAd() {
1785
+ if (this.state.adDeliveryProfile?.mode === "vast_fallback" && this.state.ads.length > 0) {
1786
+ this.playDirect();
1787
+ return;
1788
+ }
1674
1789
  const mode = this.state.waterfallMode;
1675
1790
  if (mode === "programmatic_only" || mode === "programmatic_then_direct") {
1676
1791
  this.playProgrammatic();
@@ -1685,13 +1800,20 @@ var _TrillboardsAds = class _TrillboardsAds {
1685
1800
  this.programmaticPlayer.play(
1686
1801
  () => {
1687
1802
  this.state.programmaticPlaying = false;
1803
+ this.resetProgrammaticBackoff();
1688
1804
  this.emitStateChanged();
1689
1805
  this.scheduleProgrammaticRetry();
1690
1806
  },
1691
1807
  (error) => {
1692
1808
  this.state.programmaticPlaying = false;
1693
1809
  this.state.programmaticLastError = error;
1694
- this.state.programmaticRetryCount++;
1810
+ const normalizedError = String(error || "").toLowerCase();
1811
+ const isNoFillLike = normalizedError.includes("no fill") || normalizedError.includes("no ads vast response") || normalizedError.includes("waterfall_exhausted") || normalizedError === "throttled" || normalizedError === "busy";
1812
+ if (isNoFillLike) {
1813
+ this.state.programmaticRetryCount = 0;
1814
+ } else {
1815
+ this.state.programmaticRetryCount++;
1816
+ }
1695
1817
  this.emitStateChanged();
1696
1818
  if (this.state.waterfallMode === "programmatic_then_direct" && this.state.ads.length > 0) {
1697
1819
  this.playDirect();