Trusted Arbiter
在 Flutter 应用中比较 CloudX 出价和受支持的第三方出价
Trusted Arbiter 会把已加载的 CloudX 广告与您自行接入的其他平台出价进行比较,并返回应当展示的平台。Flutter 支持由 CloudX 原生 SDK 提供,涵盖 CloudX、AdMob、Google Ad Manager、Unity LevelPlay、PubMatic,以及发布商传入的自定义出价。
调用 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.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 会根据同一广告单元的历史表现进行估价,所以您无需提供价格,这也是下面的收入上报为必需项的原因。
// 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> | 仲裁为选中出价返回的元数据。 |