@trillboards/ads-sdk 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/core/config.ts
2
- var SDK_VERSION = "2.1.1";
2
+ var SDK_VERSION = "2.2.0";
3
3
  var DEFAULT_CONFIG = {
4
4
  API_BASE: "https://api.trillboards.com/v1/partner",
5
5
  CDN_BASE: "https://cdn.trillboards.com",
@@ -58,6 +58,7 @@ var DEFAULT_PUBLIC_STATE = {
58
58
  programmaticPlaying: false,
59
59
  prefetchedReady: false,
60
60
  waterfallMode: "programmatic_only",
61
+ adDeliveryProfileMode: null,
61
62
  screenId: null,
62
63
  deviceId: null
63
64
  };
@@ -85,6 +86,7 @@ function createInitialState() {
85
86
  programmaticRetryActive: false,
86
87
  programmaticRetryCount: 0,
87
88
  programmaticLastError: null,
89
+ adDeliveryProfile: null,
88
90
  initialized: false,
89
91
  screenOrientation: null,
90
92
  screenDimensions: null,
@@ -102,6 +104,7 @@ function getPublicState(internal) {
102
104
  programmaticPlaying: internal.programmaticPlaying,
103
105
  prefetchedReady: internal.prefetchedReady ?? false,
104
106
  waterfallMode: internal.waterfallMode,
107
+ adDeliveryProfileMode: internal.adDeliveryProfile?.mode ?? null,
105
108
  screenId: internal.screenId,
106
109
  deviceId: internal.deviceId
107
110
  };
@@ -362,10 +365,9 @@ var ApiClient = class {
362
365
  }
363
366
  }
364
367
  /**
365
- * Ping the heartbeat endpoint to signal this device is alive.
366
- * Returns `true` on 2xx, `false` on any error.
368
+ * Ping heartbeat endpoint and return delivery profile + queued commands.
367
369
  */
368
- async sendHeartbeat(deviceId, screenId) {
370
+ async sendHeartbeat(deviceId, screenId, payload = {}) {
369
371
  logger.debug("Sending heartbeat", { deviceId });
370
372
  try {
371
373
  const response = await fetch(`${this.apiBase}/device/${deviceId}/heartbeat`, {
@@ -375,13 +377,24 @@ var ApiClient = class {
375
377
  device_id: deviceId,
376
378
  screen_id: screenId,
377
379
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
378
- status: "active"
380
+ status: "active",
381
+ ...payload
379
382
  }),
380
383
  signal: AbortSignal.timeout(5e3)
381
384
  });
382
- return response.ok;
385
+ if (!response.ok) {
386
+ return { ok: false, data: null };
387
+ }
388
+ let data = null;
389
+ try {
390
+ const json = await response.json();
391
+ data = json?.data ?? null;
392
+ } catch {
393
+ data = null;
394
+ }
395
+ return { ok: true, data };
383
396
  } catch {
384
- return false;
397
+ return { ok: false, data: null };
385
398
  }
386
399
  }
387
400
  /**
@@ -934,7 +947,7 @@ var WaterfallEngine = class {
934
947
  };
935
948
 
936
949
  // src/player/ProgrammaticPlayer.ts
937
- var ProgrammaticPlayer = class {
950
+ var _ProgrammaticPlayer = class _ProgrammaticPlayer {
938
951
  constructor(events, timeoutMs = 12e3) {
939
952
  // ── Public state ──────────────────────────────────────────
940
953
  this.vastTagUrl = null;
@@ -1021,8 +1034,11 @@ var ProgrammaticPlayer = class {
1021
1034
  this.telemetry.recordRequest();
1022
1035
  const correlator = Date.now();
1023
1036
  const finalUrl = vastUrl.includes("correlator=") ? vastUrl.replace(/correlator=[^&]*/, `correlator=${correlator}`) : `${vastUrl}${vastUrl.includes("?") ? "&" : "?"}correlator=${correlator}`;
1024
- if (typeof google === "undefined" || !google?.ima) {
1025
- onError("Google IMA SDK not loaded");
1037
+ const imaReady = await this.ensureImaSdk();
1038
+ if (!imaReady) {
1039
+ const msg = "Google IMA SDK not loaded";
1040
+ this.events.emit("programmatic_error", { error: msg, code: void 0 });
1041
+ onError(msg);
1026
1042
  this.telemetry.recordError();
1027
1043
  this.waterfallEngine.recordFailure(sourceName);
1028
1044
  return;
@@ -1039,6 +1055,50 @@ var ProgrammaticPlayer = class {
1039
1055
  onError(err instanceof Error ? err.message : String(err));
1040
1056
  }
1041
1057
  }
1058
+ /**
1059
+ * Ensure Google IMA SDK is available. If already loaded, resolves
1060
+ * immediately. Otherwise injects the `<script>` tag and waits.
1061
+ * Matches the Lite SDK's `loadImaScript()` pattern.
1062
+ */
1063
+ async ensureImaSdk() {
1064
+ if (typeof google !== "undefined" && google?.ima) return true;
1065
+ if (typeof document === "undefined") return false;
1066
+ const existing = document.querySelector(
1067
+ `script[src="${_ProgrammaticPlayer.IMA_SDK_URL}"]`
1068
+ );
1069
+ if (existing) {
1070
+ return new Promise((resolve) => {
1071
+ const start = Date.now();
1072
+ const check = () => {
1073
+ if (typeof google !== "undefined" && google?.ima) {
1074
+ resolve(true);
1075
+ } else if (Date.now() - start > 5e3) {
1076
+ resolve(false);
1077
+ } else {
1078
+ setTimeout(check, 100);
1079
+ }
1080
+ };
1081
+ check();
1082
+ });
1083
+ }
1084
+ if (!_ProgrammaticPlayer.imaLoadPromise) {
1085
+ _ProgrammaticPlayer.imaLoadPromise = new Promise((resolve) => {
1086
+ const script = document.createElement("script");
1087
+ script.src = _ProgrammaticPlayer.IMA_SDK_URL;
1088
+ script.async = true;
1089
+ script.onload = () => {
1090
+ _ProgrammaticPlayer.imaLoadPromise = null;
1091
+ resolve(true);
1092
+ };
1093
+ script.onerror = () => {
1094
+ _ProgrammaticPlayer.imaLoadPromise = null;
1095
+ resolve(false);
1096
+ };
1097
+ document.head.appendChild(script);
1098
+ });
1099
+ }
1100
+ return _ProgrammaticPlayer.imaLoadPromise;
1101
+ }
1042
1102
  // ── IMA request / playback lifecycle ──────────────────────
1043
1103
  async requestAdsViaIMA(vastUrl, onComplete, onError) {
1044
1104
  return new Promise((resolve) => {
@@ -1240,6 +1300,12 @@ var ProgrammaticPlayer = class {
1240
1300
  this.telemetry.reset();
1241
1301
  }
1242
1302
  };
1303
+ // ── IMA SDK loader ──────────────────────────────────────────
1304
+ /** IMA script URL */
1305
+ _ProgrammaticPlayer.IMA_SDK_URL = "https://imasdk.googleapis.com/js/sdkloader/ima3.js";
1306
+ /** Singleton load promise so concurrent calls don't insert multiple scripts. */
1307
+ _ProgrammaticPlayer.imaLoadPromise = null;
1308
+ var ProgrammaticPlayer = _ProgrammaticPlayer;
1243
1309
 
1244
1310
  // src/player/Player.ts
1245
1311
  var MAX_AD_DURATION_SECONDS = 300;
@@ -1674,11 +1740,104 @@ var _TrillboardsAds = class _TrillboardsAds {
1674
1740
  }
1675
1741
  startHeartbeatTimer() {
1676
1742
  if (this.state.heartbeatTimer) clearInterval(this.state.heartbeatTimer);
1743
+ const tick = () => {
1744
+ this.sendHeartbeatTick().catch(() => {
1745
+ });
1746
+ };
1747
+ tick();
1677
1748
  this.state.heartbeatTimer = setInterval(() => {
1678
- this.api.sendHeartbeat(this.config.deviceId, this.state.screenId);
1749
+ tick();
1679
1750
  }, this.config.heartbeatInterval);
1680
1751
  }
1752
+ async sendHeartbeatTick() {
1753
+ const result = await this.api.sendHeartbeat(
1754
+ this.config.deviceId,
1755
+ this.state.screenId,
1756
+ this.buildHeartbeatPayload()
1757
+ );
1758
+ if (!result.ok || !result.data) return;
1759
+ if (result.data.ad_delivery_profile?.mode) {
1760
+ this.applyAdDeliveryProfile(result.data.ad_delivery_profile);
1761
+ }
1762
+ if (Array.isArray(result.data.commands) && result.data.commands.length > 0) {
1763
+ this.runHeartbeatCommands(result.data.commands);
1764
+ }
1765
+ }
1766
+ buildHeartbeatPayload() {
1767
+ const userAgent = typeof navigator !== "undefined" ? navigator.userAgent : "";
1768
+ const platform = typeof navigator !== "undefined" ? navigator.platform : null;
1769
+ const connection = typeof navigator !== "undefined" ? navigator.connection : null;
1770
+ const chromiumMatch = userAgent.match(/(?:Chrome|Chromium)\/(\d+)/i);
1771
+ const chromiumMajor = chromiumMatch ? Number(chromiumMatch[1]) : void 0;
1772
+ const imaSupported = typeof window !== "undefined" ? Boolean(window?.google?.ima) : null;
1773
+ const telemetry = {
1774
+ powerState: typeof document !== "undefined" && document.hidden ? "standby" : "on",
1775
+ agentStatus: typeof document !== "undefined" && document.hidden ? "background" : "foreground",
1776
+ os: platform || void 0,
1777
+ agentVersion: SDK_VERSION,
1778
+ ima_integration: "html5_webview",
1779
+ ima_supported: imaSupported,
1780
+ webview_chromium_major: Number.isFinite(chromiumMajor) ? chromiumMajor : void 0
1781
+ };
1782
+ const payload = {
1783
+ telemetry,
1784
+ sdk: {
1785
+ version: SDK_VERSION,
1786
+ ima_supported: imaSupported,
1787
+ ima_integration: "html5_webview",
1788
+ webview_chromium_major: Number.isFinite(chromiumMajor) ? chromiumMajor : void 0
1789
+ },
1790
+ device: {
1791
+ os: platform || void 0,
1792
+ model: userAgent || void 0
1793
+ }
1794
+ };
1795
+ if (connection) {
1796
+ payload.network = {
1797
+ connectionType: connection.type || void 0,
1798
+ effectiveType: connection.effectiveType || void 0,
1799
+ downlinkMbps: typeof connection.downlink === "number" ? connection.downlink : void 0,
1800
+ bandwidthMbps: typeof connection.downlink === "number" ? connection.downlink : void 0,
1801
+ rtt: typeof connection.rtt === "number" ? connection.rtt : void 0,
1802
+ latencyRtt: typeof connection.rtt === "number" ? connection.rtt : void 0,
1803
+ saveData: typeof connection.saveData === "boolean" ? connection.saveData : void 0
1804
+ };
1805
+ }
1806
+ return payload;
1807
+ }
1808
+ applyAdDeliveryProfile(profile) {
1809
+ if (!profile?.mode) return;
1810
+ const previousMode = this.state.adDeliveryProfile?.mode ?? null;
1811
+ this.state.adDeliveryProfile = profile;
1812
+ if (profile.mode === "vast_fallback" && this.state.programmaticPlaying) {
1813
+ this.programmaticPlayer.stop({ silent: true });
1814
+ this.state.programmaticPlaying = false;
1815
+ }
1816
+ if (previousMode !== profile.mode) {
1817
+ this.emitStateChanged();
1818
+ }
1819
+ }
1820
+ runHeartbeatCommands(commands) {
1821
+ for (const command of commands) {
1822
+ const name = String(command?.command || "").toLowerCase();
1823
+ if (name === "refresh_ads") {
1824
+ this.refresh().catch(() => {
1825
+ });
1826
+ } else if (name === "restart") {
1827
+ if (typeof window !== "undefined" && typeof window.location?.reload === "function") {
1828
+ window.location.reload();
1829
+ return;
1830
+ }
1831
+ this.refresh().catch(() => {
1832
+ });
1833
+ }
1834
+ }
1835
+ }
1681
1836
  playNextAd() {
1837
+ if (this.state.adDeliveryProfile?.mode === "vast_fallback" && this.state.ads.length > 0) {
1838
+ this.playDirect();
1839
+ return;
1840
+ }
1682
1841
  const mode = this.state.waterfallMode;
1683
1842
  if (mode === "programmatic_only" || mode === "programmatic_then_direct") {
1684
1843
  this.playProgrammatic();
@@ -1693,13 +1852,20 @@ var _TrillboardsAds = class _TrillboardsAds {
1693
1852
  this.programmaticPlayer.play(
1694
1853
  () => {
1695
1854
  this.state.programmaticPlaying = false;
1855
+ this.resetProgrammaticBackoff();
1696
1856
  this.emitStateChanged();
1697
1857
  this.scheduleProgrammaticRetry();
1698
1858
  },
1699
1859
  (error) => {
1700
1860
  this.state.programmaticPlaying = false;
1701
1861
  this.state.programmaticLastError = error;
1702
- this.state.programmaticRetryCount++;
1862
+ const normalizedError = String(error || "").toLowerCase();
1863
+ const isNoFillLike = normalizedError.includes("no fill") || normalizedError.includes("no ads vast response") || normalizedError.includes("waterfall_exhausted") || normalizedError === "throttled" || normalizedError === "busy";
1864
+ if (isNoFillLike) {
1865
+ this.state.programmaticRetryCount = 0;
1866
+ } else {
1867
+ this.state.programmaticRetryCount++;
1868
+ }
1703
1869
  this.emitStateChanged();
1704
1870
  if (this.state.waterfallMode === "programmatic_then_direct" && this.state.ads.length > 0) {
1705
1871
  this.playDirect();
@@ -1821,12 +1987,12 @@ var TrillboardsError = class extends Error {
1821
1987
  var TrillboardsAuthenticationError = class extends TrillboardsError {
1822
1988
  constructor(message) {
1823
1989
  super(
1824
- message ?? "Invalid API key. Check your key at https://trillboards.com/developers",
1990
+ message ?? "Invalid API key. Check your key at https://trillboards.com/support/developers",
1825
1991
  {
1826
1992
  type: "authentication_error",
1827
1993
  code: "invalid_api_key",
1828
1994
  statusCode: 401,
1829
- help: "https://trillboards.com/developers"
1995
+ help: "https://trillboards.com/support/developers"
1830
1996
  }
1831
1997
  );
1832
1998
  this.name = "TrillboardsAuthenticationError";