Trusted Arbiter
在 Flutter 应用中比较 CloudX 出价和受支持的第三方出价
Trusted Arbiter 会把已加载的 CloudX 广告与您自行接入的其他平台出价进行比较,并返回应当展示的平台。Flutter 支持由 CloudX 原生 SDK 提供,涵盖 CloudX、AdMob、Google Ad Manager、Unity LevelPlay、PubMatic,以及发布商传入的自定义出价。
为什么使用 Trusted Arbiter?
当应用从多个平台加载广告时,需要决定展示哪个广告。Trusted Arbiter 为受支持的出价提供统一的比较 API。
减少需要维护的出价比较逻辑
将受支持的出价提交给 Trusted Arbiter,根据返回的平台选择要展示的广告。应用仍需负责合作伙伴 SDK 的集成、广告加载和展示。
使用可用的出价值
在可以获取实际出价值时,Trusted Arbiter 可以使用这些值进行比较。部分广告需求使用估算价格。请参阅下文中各类受支持输入的定价说明。
调用 CloudX.arbiter 前,CloudX.initialize 必须已经完成。为每条已加载的广告构建一个出价,然后交给它:
import 'package:cloudx_flutter/cloudx.dart';
import 'package:flutter/foundation.dart';
// cloudXAd 是您在 onAdLoaded 中收到的 CloudXAd,请原样传入。
final result = await CloudX.arbiter(CloudXArbiterConfiguration(bids: [
CloudXArbiterBid.cloudX(cloudXAd),
CloudXArbiterBid.adMob(adUnitId: adMobAdUnitId),
]));
debugPrint('选中的平台:${result.platform}');result.platform 的类型是 CloudXArbiterPlatform。请与其常量进行比较:
if (result.platform == CloudXArbiterPlatform.cloudX) {
// 展示 CloudX 广告
} else if (result.platform == CloudXArbiterPlatform.adMob) {
// 展示您的 AdMob 广告
}可用常量为 cloudX、adMob、gam、levelPlay、pubMatic、custom 和 none。
Trusted Arbiter 需要在 CloudX 控制台中为您的应用启用。在启用之前,每次调用仍由本地兜底应答,而兜底只比较带有本地可比价格的出价。未设置手动价格的 Google 出价没有可在本地比较的价格,因此在与已定价的出价比较时会落败,但当它是唯一有效出价时仍会胜出。通过 manualRevenuePerImpressionUSD 自行定价的出价则可以正常参与比较。
示例应用
CloudX Flutter 示例应用以插屏广告演示了本页描述的流程:CloudX 和 AdMob 并行加载,两个填充都作为出价参与,由 CloudX.arbiter 选出胜出方,并从保存的结果中展示,因此展示路径不发起任何网络请求。它是在真机上验证过的版本,出现问题时也是优先修复的版本。
arbiter_interstitial_controller.dart
完整的加载、仲裁与展示流程,两个 SDK 的调用都集中在这一个文件里。
sdk_startup.dart
按必须的顺序启动两个 SDK。
demo_config.dart
分平台的应用密钥和广告单元 ID。自己运行时首先要改的就是这个文件。
cloudx_failure_text.dart
把 CloudX 的失败整理成一行,并带上 SDK 为该错误码定义的名称。
示例应用使用 Google 的公开 AdMob 测试广告单元,它们上报的收入为 0.0。0.0 的价格会被丢弃,因此 AdMob 出价到达仲裁时没有可比价格。改为指向真实投放的广告单元,即可看到价格之间的比较。
何时运行仲裁
请在候选广告加载完成后运行仲裁,绝不要放在展示路径上。在功能已启用且有多个候选时,CloudX.arbiter 会请求服务端,而点击按钮看广告的用户不应为网络请求等待。
并行加载所有候选。当它们全部有结果(加载成功或失败)之后运行仲裁并保存结果。到了广告位处,立即展示已保存的胜出方,不再发起网络请求。广告关闭、展示失败或候选过期之后,再跑一轮。
CloudXArbiterResult? nextWinner;
int arbiterRound = 0;
// 两侧都有结果后运行,早于广告位。
// 只有真正加载成功的候选才会成为出价:为加载失败的广告单元出价,可能凭价格历史
// 胜出,最终却没有广告可展示。
Future<void> prepareWinner(CloudXAd? cloudXAd, String? loadedAdMobAdUnitId) async {
/*
* 广告位可以在本次调用尚未返回时先展示单个候选并启动下一轮,因此多轮可能重
* 叠。为每一轮编号,并且只允许最新的一轮写入结果:否则较早的一轮会最后返回,
* 用已经展示过的广告覆盖当前胜出方。
*/
final round = ++arbiterRound;
// 新的一轮意味着候选已经变化,上一轮留在这里的结果指向的广告已不存在。请立即
// 清空,避免本轮还在进行时广告位展示它。
nextWinner = null;
final bids = <CloudXArbiterBid>[
if (cloudXAd != null) CloudXArbiterBid.cloudX(cloudXAd),
if (loadedAdMobAdUnitId != null)
CloudXArbiterBid.adMob(adUnitId: loadedAdMobAdUnitId),
];
if (bids.isEmpty) {
return; // 两侧都没有填充,无需仲裁
}
try {
final result =
await CloudX.arbiter(CloudXArbiterConfiguration(bids: bids));
if (round != arbiterRound) {
return; // 本轮进行期间已开始更新的一轮,以新的一轮结果为准
}
// none 不是胜出方:保存它会导致既没有可展示的广告,也没有需要重新加载的
// 广告,因为两侧都还持有各自的填充。
nextWinner = result.platform == CloudXArbiterPlatform.none ? null : result;
} catch (error) {
// 该方法由加载回调触发,没有任何代码在等待它:未捕获的平台通道异常会变成
// 未处理的异步错误。已加载的广告仍然保留,下一轮可以重新运行。
if (round == arbiterRound) {
nextWinner = null;
}
debugPrint('仲裁失败:$error');
}
}
// 在广告位处运行,这里没有网络请求。
void showAd(String cloudXAdUnitId) {
final winner = nextWinner;
nextWinner = null;
/*
* 广告位结束了本轮周期,因此让仍在进行的轮次失效。否则在广告位之前启动的那一
* 轮返回时仍是最新一轮,会写入一个指向本次已展示或已跳过的广告的胜出方。
*/
arbiterRound++;
if (winner == null) {
return; // 没有准备好的胜出方,继续流程即可
}
if (winner.platform == CloudXArbiterPlatform.cloudX) {
// 本轮准备的是插屏广告。激励视频广告的流程完全相同,只需改用
// showRewarded,并传入激励视频广告的广告单元 ID。
CloudX.showInterstitial(adUnitId: cloudXAdUnitId);
} else if (winner.platform == CloudXArbiterPlatform.adMob) {
// 展示您的 AdMob 插屏广告
} else {
// 本示例只提交 CloudX 和 AdMob 两种出价。您新增的每个工厂都需要在这里补上
// 自己的分支,否则它获胜时什么都不会展示。
debugPrint('没有对应的展示分支:${winner.platform}');
}
}如果广告位比胜出方来得更早,可以跳过广告,或者展示唯一加载成功的那个候选。这是可接受的降级路径,但不应成为常态。
返回 none 的那一轮会让两个候选都继续持有各自的填充,而持有填充本身不会触发任何后续:没有广告被展示,也就不会有 hidden 回调或展示失败来启动下一轮。此时请走降级路径,而不是什么都不展示,并让该广告的 hidden 回调重新开始一轮。none 之后什么都不展示,会让这个广告位一直空着,直到某个候选过期、其替补加载完成为止。
请在 CloudX 广告的 hidden 回调中销毁刚展示的广告,然后再开始下一轮。这样下一次加载会立即创建新的实例并发起新的竞价。另一侧的网络也请同样处理。
没有任何代码在等待这一轮,因此请在调用仲裁的位置捕获平台通道异常,避免它变成未被处理的异步错误。已加载的广告在失败的一轮之后依然保留,下一轮可以直接重跑。
支持的广告格式
仲裁与格式无关:它接受任何已加载的 CloudX 广告,字段映射在各格式下都相同。
- 全屏广告:插屏和激励视频。 请按上文在广告位之前准备好胜出方。
- 视图类广告:横幅和 MREC。 没有用户操作在等待,因此可以在
await CloudX.arbiter返回处直接渲染胜出方。参见横幅广告和 MREC。
AdMob 与 Google Ad Manager
CloudX 会把已加载的 CloudX 广告与已加载的 AdMob 或 Ad Manager 广告进行比较。AdMob 和 Ad Manager 是彼此独立的需求方,可以在同一次仲裁中同时出价。
Google 需求方通常不会在展示之前公开已加载广告的价格,因此没有可供仲裁比较的预出价价格。CloudX 会根据您的 Google 历史表现自动为出价定价,因此您无需提供价格。下面的收入上报会为这份历史提供数据,这也是它为必需项的原因。参见 AdMob/GAM estimated 定价的工作方式。
// networkName 为可选;当 Google 提供竞得的广告来源时可以传入。
final adMobBid = CloudXArbiterBid.adMob(
adUnitId: adMobAdUnitId,
networkName: adMobAdSourceName,
);
// Ad Manager 的广告单元 ID 形如 /NNNNNNN/placement/name。
final adManagerBid = CloudXArbiterBid.gam(
adUnitId: '/21775744923/example/interstitial',
);
final result = await CloudX.arbiter(CloudXArbiterConfiguration(bids: [
CloudXArbiterBid.cloudX(cloudXAd),
adMobBid,
adManagerBid,
]));胜出的 Google 出价会上报自己的平台:CloudXArbiterPlatform.adMob 或 CloudXArbiterPlatform.gam,因此不必像自定义出价那样去查看 platformName 来区分两者。
广告单元 ID 为空时仍会构建出价而不会让应用崩溃,但该出价没有可用的身份:它不会被定价,服务端也会拒绝它。
把 Google 付费事件回传给 CloudX
上报 Google 的展示级收入是对 AdMob 与 Ad Manager 出价进行仲裁时的必需环节,而不是可选的数据统计。若应用接入了 Google 需求但不使用 Trusted Arbiter,则无需上报。缺少这些事件,CloudX 就无从得知 Google 需求方的实际价格,后续仲裁中的估价也会随之变差。
请在 Google 的付费事件回调中调用 CloudX.reportRevenueData。CloudXRevenueData.revenue 使用货币单位,而 google_mobile_ads 在两个平台上都上报 valueMicros,因此两端都需要除以 1,000,000。
此流程基于 google_mobile_ads 包,它封装的是 Google Mobile Ads SDK。在 Android 上与之配套的是 io.cloudx:adapter-googlewaterfall,而不是 io.cloudx:adapter-admob:两代 Google SDK 的类相互冲突,无法安装在同一个应用中。无论是否启用 CloudX 的 Google 适配器,该包都需要在原生层声明 Google 应用标识,因此请在运行本流程前完成 iOS 和 Android 设置中的相应配置。仅接入 Ad Manager 需求的 Android 应用应在那里声明 AD_MANAGER_APP,而不是 AdMob 应用 ID。
import 'package:cloudx_flutter/cloudx.dart';
import 'package:flutter/foundation.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
// 把它挂到您加载的广告上,并传入该出价对应的平台和广告单元 ID:AdMob 单元配
// CloudXRevenuePlatform.adMob,Ad Manager 单元配 CloudXRevenuePlatform.gam。
// 两者始终成对出现,因此 Ad Manager 的展示不会被上报成 AdMob。adFormat 是这条
// 广告的格式:'banner'、'mrec'、'interstitial' 或 'rewarded'。
void reportGooglePaidEvents(
Ad ad,
CloudXRevenuePlatform platform,
String googleAdUnitId,
String adFormat,
) {
ad.onPaidEvent = (paidAd, valueMicros, precision, currencyCode) async {
try {
final accepted = await CloudX.reportRevenueData(CloudXRevenueData(
platform: platform,
revenue: valueMicros / 1000000.0,
adFormat: adFormat,
currencyCode: currencyCode,
precision: _cloudXPrecision(precision),
adUnitId: googleAdUnitId,
));
if (!accepted) {
debugPrint('CloudX 丢弃了本次收入上报');
}
} catch (error) {
// 该回调由 Google 触发,没有任何代码等待它,未捕获的平台通道异常会变成
// 未处理的异步错误。
debugPrint('reportRevenueData 失败:$error');
}
};
}
CloudXRevenuePrecision _cloudXPrecision(PrecisionType precision) {
switch (precision) {
case PrecisionType.precise:
return CloudXRevenuePrecision.exact;
case PrecisionType.estimated:
return CloudXRevenuePrecision.estimated;
case PrecisionType.publisherProvided:
return CloudXRevenuePrecision.publisherDefined;
case PrecisionType.unknown:
return CloudXRevenuePrecision.undefined;
}
}请上报您传给出价的同一个广告单元 ID,这样 CloudX 才能把实际价格归属到正确的广告单元。当数据被丢弃时 reportRevenueData 返回 false,例如 SDK 尚未初始化、平台名称为空,或收入不是有限数值。完整字段说明参见发布商上报的收入数据。
返回 true 并不表示该价格已进入 CloudX 的价格历史。价格历史只保留带广告单元 ID 的正数美元金额(未填写货币代码时按美元处理),因此收入为零的上报会被接受,但不用于定价。Google 的测试广告单元收入为零,这也是在使用测试单元期间 AdMob 出价始终无法参与价格比较的原因。
使用预出价 ILRD 手动传值
部分 AdMob 账户可以在预出价阶段拿到展示级收入数据:广告值在加载时(展示之前)即可获得。这是按账户开通的历史能力,请与您的 Google 客户团队确认是否已启用。只有在展示前就已知的确切单次展示价格,才比 CloudX 的估价更有优势。
final adMobBid = CloudXArbiterBid.adMob(
adUnitId: adMobAdUnitId,
manualRevenuePerImpressionUSD: preBidPricePerImpressionUSD,
);manualRevenuePerImpressionUSD 是单次展示的收入,单位为美元,不是 CPM;非美元金额需先换算。取值处理规则:
0.0是一个真实价格,表示该出价没有价值,而不是价格缺失。- 负值和非有限值不是价格,会被视为未提供并记录日志。
- 广告单元 ID 为空时手动价格会被丢弃,因为没有身份的出价无法校验。
具体单位取决于底层原生 SDK。AdMob 广告值的各平台换算规则参见 Android 和 iOS 页面。
横幅广告和 MREC
自动刷新与仲裁相冲突:一次刷新可能在仲裁已经选出胜出方之后替换掉广告。请在 CloudX 控制台中关闭自动刷新,对 CloudX 广告调用停止方法,并同时关闭其他参与仲裁的网络的自动刷新。
CloudX.createBanner(
adUnitId: adUnitId,
position: CloudXAdViewPosition.bottomCenter,
);
// 创建广告就会启动刷新循环,因此紧接着停止它。
CloudX.stopBannerAutoRefresh(adUnitId: adUnitId);只展示胜出出价对应的广告,其余保持隐藏,并使用程序化浮层 API:createBanner 或 createMrec 负责加载候选,仲裁返回后再用 showBanner / hideBanner 决定用户看到哪一个。
胜出方产生展示之后,只向胜出的网络请求新的填充,保留其他已有填充的广告,只对没有填充的网络重新请求,待响应返回后再跑一次仲裁。展示中的广告建议每 20 到 30 秒刷新一次,间隔更短会降低 CPM 表现。
其他中介平台
CloudXArbiterBid.levelPlay 接受 Unity LevelPlay 自己上报的数值,其中 precision 是 LevelPlay 的原始精度标记:
final levelPlayBid = CloudXArbiterBid.levelPlay(
networkName: levelPlayAdInfo.adNetwork,
revenue: levelPlayAdInfo.revenue,
precision: levelPlayAdInfo.precision,
);CloudXArbiterBid.pubMatic 接受来自 OpenWrap 出价对象的价格:
final pubMaticBid = CloudXArbiterBid.pubMatic(
price: pubMaticPrice,
partnerName: pubMaticPartnerName,
);自定义出价
当某个平台没有专门的工厂方法时,请用 CloudXArbiterBid.custom 把它与 CloudX 进行比较。
final customBid = CloudXArbiterBid.custom(
platformName: 'my_mediation_platform',
networkName: 'winning_demand_source',
revenuePerImpressionUSD: 0.00125,
precision: CloudXArbiterPrecision.exact,
extras: {'ad_unit': 'third-party-ad-unit-id'},
);
final result = await CloudX.arbiter(CloudXArbiterConfiguration(bids: [
CloudXArbiterBid.cloudX(cloudXAd),
customBid,
]));自定义出价胜出时,result.platform 为 CloudXArbiterPlatform.custom,result.platformName 则是您传入的 platformName。revenuePerImpressionUSD 是单次展示的美元收入,不是 CPM。CloudXArbiterPrecision 提供 exact、estimated、publisherDefined 和 undefined,CloudXArbiterPrecision.of 也接受您自己 SDK 的原始标记。networkName 为必填;如果您的来源没有竞得网络名称,请传 ''。缺少非空 platformName、收入或精度的自定义出价会被丢弃并记录警告,不参与竞争。
结果
| 属性 | 类型 | 说明 |
|---|---|---|
platform | CloudXArbiterPlatform | 胜出的平台,或 none。 |
platformName | String | 具体平台名称;自定义出价胜出时为您传入的 platformName。 |
bidId | String? | 选中出价的标识;没有胜出方时为 null。 |
id | String | 本次仲裁请求的标识。 |
extras | Map<String, String> | 仲裁为选中出价返回的元数据。 |