@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/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
  };
@@ -360,10 +366,9 @@ var ApiClient = class {
360
366
  }
361
367
  }
362
368
  /**
363
- * Ping the heartbeat endpoint to signal this device is alive.
364
- * Returns `true` on 2xx, `false` on any error.
369
+ * Ping heartbeat endpoint and return delivery profile + queued commands.
365
370
  */
366
- async sendHeartbeat(deviceId, screenId) {
371
+ async sendHeartbeat(deviceId, screenId, payload = {}) {
367
372
  logger.debug("Sending heartbeat", { deviceId });
368
373
  try {
369
374
  const response = await fetch(`${this.apiBase}/device/${deviceId}/heartbeat`, {
@@ -373,13 +378,24 @@ var ApiClient = class {
373
378
  device_id: deviceId,
374
379
  screen_id: screenId,
375
380
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
376
- status: "active"
381
+ status: "active",
382
+ ...payload
377
383
  }),
378
384
  signal: AbortSignal.timeout(5e3)
379
385
  });
380
- 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 };
381
397
  } catch {
382
- return false;
398
+ return { ok: false, data: null };
383
399
  }
384
400
  }
385
401
  /**
@@ -932,7 +948,7 @@ var WaterfallEngine = class {
932
948
  };
933
949
 
934
950
  // src/player/ProgrammaticPlayer.ts
935
- var ProgrammaticPlayer = class {
951
+ var _ProgrammaticPlayer = class _ProgrammaticPlayer {
936
952
  constructor(events, timeoutMs = 12e3) {
937
953
  // ── Public state ──────────────────────────────────────────
938
954
  this.vastTagUrl = null;
@@ -1019,8 +1035,11 @@ var ProgrammaticPlayer = class {
1019
1035
  this.telemetry.recordRequest();
1020
1036
  const correlator = Date.now();
1021
1037
  const finalUrl = vastUrl.includes("correlator=") ? vastUrl.replace(/correlator=[^&]*/, `correlator=${correlator}`) : `${vastUrl}${vastUrl.includes("?") ? "&" : "?"}correlator=${correlator}`;
1022
- if (typeof google === "undefined" || !google?.ima) {
1023
- onError("Google IMA SDK not loaded");
1038
+ const imaReady = await this.ensureImaSdk();
1039
+ if (!imaReady) {
1040
+ const msg = "Google IMA SDK not loaded";
1041
+ this.events.emit("programmatic_error", { error: msg, code: void 0 });
1042
+ onError(msg);
1024
1043
  this.telemetry.recordError();
1025
1044
  this.waterfallEngine.recordFailure(sourceName);
1026
1045
  return;
@@ -1037,6 +1056,50 @@ var ProgrammaticPlayer = class {
1037
1056
  onError(err instanceof Error ? err.message : String(err));
1038
1057
  }
1039
1058
  }
1059
+ /**
1060
+ * Ensure Google IMA SDK is available. If already loaded, resolves
1061
+ * immediately. Otherwise injects the `<script>` tag and waits.
1062
+ * Matches the Lite SDK's `loadImaScript()` pattern.
1063
+ */
1064
+ async ensureImaSdk() {
1065
+ if (typeof google !== "undefined" && google?.ima) return true;
1066
+ if (typeof document === "undefined") return false;
1067
+ const existing = document.querySelector(
1068
+ `script[src="${_ProgrammaticPlayer.IMA_SDK_URL}"]`
1069
+ );
1070
+ if (existing) {
1071
+ return new Promise((resolve) => {
1072
+ const start = Date.now();
1073
+ const check = () => {
1074
+ if (typeof google !== "undefined" && google?.ima) {
1075
+ resolve(true);
1076
+ } else if (Date.now() - start > 5e3) {
1077
+ resolve(false);
1078
+ } else {
1079
+ setTimeout(check, 100);
1080
+ }
1081
+ };
1082
+ check();
1083
+ });
1084
+ }
1085
+ if (!_ProgrammaticPlayer.imaLoadPromise) {
1086
+ _ProgrammaticPlayer.imaLoadPromise = new Promise((resolve) => {
1087
+ const script = document.createElement("script");
1088
+ script.src = _ProgrammaticPlayer.IMA_SDK_URL;
1089
+ script.async = true;
1090
+ script.onload = () => {
1091
+ _ProgrammaticPlayer.imaLoadPromise = null;
1092
+ resolve(true);
1093
+ };
1094
+ script.onerror = () => {
1095
+ _ProgrammaticPlayer.imaLoadPromise = null;
1096
+ resolve(false);
1097
+ };
1098
+ document.head.appendChild(script);
1099
+ });
1100
+ }
1101
+ return _ProgrammaticPlayer.imaLoadPromise;
1102
+ }
1040
1103
  // ── IMA request / playback lifecycle ──────────────────────
1041
1104
  async requestAdsViaIMA(vastUrl, onComplete, onError) {
1042
1105
  return new Promise((resolve) => {
@@ -1238,6 +1301,12 @@ var ProgrammaticPlayer = class {
1238
1301
  this.telemetry.reset();
1239
1302
  }
1240
1303
  };
1304
+ // ── IMA SDK loader ──────────────────────────────────────────
1305
+ /** IMA script URL */
1306
+ _ProgrammaticPlayer.IMA_SDK_URL = "https://imasdk.googleapis.com/js/sdkloader/ima3.js";
1307
+ /** Singleton load promise so concurrent calls don't insert multiple scripts. */
1308
+ _ProgrammaticPlayer.imaLoadPromise = null;
1309
+ var ProgrammaticPlayer = _ProgrammaticPlayer;
1241
1310
 
1242
1311
  // src/player/Player.ts
1243
1312
  var MAX_AD_DURATION_SECONDS = 300;
@@ -1672,11 +1741,104 @@ var _TrillboardsAds = class _TrillboardsAds {
1672
1741
  }
1673
1742
  startHeartbeatTimer() {
1674
1743
  if (this.state.heartbeatTimer) clearInterval(this.state.heartbeatTimer);
1744
+ const tick = () => {
1745
+ this.sendHeartbeatTick().catch(() => {
1746
+ });
1747
+ };
1748
+ tick();
1675
1749
  this.state.heartbeatTimer = setInterval(() => {
1676
- this.api.sendHeartbeat(this.config.deviceId, this.state.screenId);
1750
+ tick();
1677
1751
  }, this.config.heartbeatInterval);
1678
1752
  }
1753
+ async sendHeartbeatTick() {
1754
+ const result = await this.api.sendHeartbeat(
1755
+ this.config.deviceId,
1756
+ this.state.screenId,
1757
+ this.buildHeartbeatPayload()
1758
+ );
1759
+ if (!result.ok || !result.data) return;
1760
+ if (result.data.ad_delivery_profile?.mode) {
1761
+ this.applyAdDeliveryProfile(result.data.ad_delivery_profile);
1762
+ }
1763
+ if (Array.isArray(result.data.commands) && result.data.commands.length > 0) {
1764
+ this.runHeartbeatCommands(result.data.commands);
1765
+ }
1766
+ }
1767
+ buildHeartbeatPayload() {
1768
+ const userAgent = typeof navigator !== "undefined" ? navigator.userAgent : "";
1769
+ const platform = typeof navigator !== "undefined" ? navigator.platform : null;
1770
+ const connection = typeof navigator !== "undefined" ? navigator.connection : null;
1771
+ const chromiumMatch = userAgent.match(/(?:Chrome|Chromium)\/(\d+)/i);
1772
+ const chromiumMajor = chromiumMatch ? Number(chromiumMatch[1]) : void 0;
1773
+ const imaSupported = typeof window !== "undefined" ? Boolean(window?.google?.ima) : null;
1774
+ const telemetry = {
1775
+ powerState: typeof document !== "undefined" && document.hidden ? "standby" : "on",
1776
+ agentStatus: typeof document !== "undefined" && document.hidden ? "background" : "foreground",
1777
+ os: platform || void 0,
1778
+ agentVersion: SDK_VERSION,
1779
+ ima_integration: "html5_webview",
1780
+ ima_supported: imaSupported,
1781
+ webview_chromium_major: Number.isFinite(chromiumMajor) ? chromiumMajor : void 0
1782
+ };
1783
+ const payload = {
1784
+ telemetry,
1785
+ sdk: {
1786
+ version: SDK_VERSION,
1787
+ ima_supported: imaSupported,
1788
+ ima_integration: "html5_webview",
1789
+ webview_chromium_major: Number.isFinite(chromiumMajor) ? chromiumMajor : void 0
1790
+ },
1791
+ device: {
1792
+ os: platform || void 0,
1793
+ model: userAgent || void 0
1794
+ }
1795
+ };
1796
+ if (connection) {
1797
+ payload.network = {
1798
+ connectionType: connection.type || void 0,
1799
+ effectiveType: connection.effectiveType || void 0,
1800
+ downlinkMbps: typeof connection.downlink === "number" ? connection.downlink : void 0,
1801
+ bandwidthMbps: typeof connection.downlink === "number" ? connection.downlink : void 0,
1802
+ rtt: typeof connection.rtt === "number" ? connection.rtt : void 0,
1803
+ latencyRtt: typeof connection.rtt === "number" ? connection.rtt : void 0,
1804
+ saveData: typeof connection.saveData === "boolean" ? connection.saveData : void 0
1805
+ };
1806
+ }
1807
+ return payload;
1808
+ }
1809
+ applyAdDeliveryProfile(profile) {
1810
+ if (!profile?.mode) return;
1811
+ const previousMode = this.state.adDeliveryProfile?.mode ?? null;
1812
+ this.state.adDeliveryProfile = profile;
1813
+ if (profile.mode === "vast_fallback" && this.state.programmaticPlaying) {
1814
+ this.programmaticPlayer.stop({ silent: true });
1815
+ this.state.programmaticPlaying = false;
1816
+ }
1817
+ if (previousMode !== profile.mode) {
1818
+ this.emitStateChanged();
1819
+ }
1820
+ }
1821
+ runHeartbeatCommands(commands) {
1822
+ for (const command of commands) {
1823
+ const name = String(command?.command || "").toLowerCase();
1824
+ if (name === "refresh_ads") {
1825
+ this.refresh().catch(() => {
1826
+ });
1827
+ } else if (name === "restart") {
1828
+ if (typeof window !== "undefined" && typeof window.location?.reload === "function") {
1829
+ window.location.reload();
1830
+ return;
1831
+ }
1832
+ this.refresh().catch(() => {
1833
+ });
1834
+ }
1835
+ }
1836
+ }
1679
1837
  playNextAd() {
1838
+ if (this.state.adDeliveryProfile?.mode === "vast_fallback" && this.state.ads.length > 0) {
1839
+ this.playDirect();
1840
+ return;
1841
+ }
1680
1842
  const mode = this.state.waterfallMode;
1681
1843
  if (mode === "programmatic_only" || mode === "programmatic_then_direct") {
1682
1844
  this.playProgrammatic();
@@ -1691,13 +1853,20 @@ var _TrillboardsAds = class _TrillboardsAds {
1691
1853
  this.programmaticPlayer.play(
1692
1854
  () => {
1693
1855
  this.state.programmaticPlaying = false;
1856
+ this.resetProgrammaticBackoff();
1694
1857
  this.emitStateChanged();
1695
1858
  this.scheduleProgrammaticRetry();
1696
1859
  },
1697
1860
  (error) => {
1698
1861
  this.state.programmaticPlaying = false;
1699
1862
  this.state.programmaticLastError = error;
1700
- this.state.programmaticRetryCount++;
1863
+ const normalizedError = String(error || "").toLowerCase();
1864
+ const isNoFillLike = normalizedError.includes("no fill") || normalizedError.includes("no ads vast response") || normalizedError.includes("waterfall_exhausted") || normalizedError === "throttled" || normalizedError === "busy";
1865
+ if (isNoFillLike) {
1866
+ this.state.programmaticRetryCount = 0;
1867
+ } else {
1868
+ this.state.programmaticRetryCount++;
1869
+ }
1701
1870
  this.emitStateChanged();
1702
1871
  if (this.state.waterfallMode === "programmatic_then_direct" && this.state.ads.length > 0) {
1703
1872
  this.playDirect();