Trusted Arbiter

在 Android 应用中比较 CloudX 出价与受支持的第三方出价

Trusted Arbiter 会比较已加载的 CloudX 出价与受支持的第三方出价,并返回选中的平台。CloudX SDK 4.1.0 及更高版本支持 CloudX、Unity LevelPlay 和 PubMatic 出价输入。CloudX SDK 4.2.0 及更高版本还支持发布商传入的自定义出价输入。

支持的广告格式

Trusted Arbiter 与广告格式无关:无论广告格式如何,它都会接收任意已加载的 CloudX 广告,并与传入的第三方出价进行比较。横幅广告、MREC、插屏广告和激励视频广告均受支持。

  • 插屏广告和激励视频广告(全屏格式)遵循本页后文展示的分步演练和控制器模式。
  • 横幅广告和 MREC(视图格式)需要横幅与 MREC 仲裁一节中介绍的额外处理,因为落选出价对应的视图绝不能被添加到视图层级中。

基础 API

从已加载的广告创建出价候选项,然后传给 CloudX.arbiter()

// cloudXAd 是 CloudX onAdLoaded 回调中的 CloudXAd 对象。
// levelPlayAdInfo 是 Unity LevelPlay 广告信息对象。
// pobBid 是 PubMatic/OpenWrap 出价对象。
val bids = listOf(
    CloudXArbiterBid.cloudX(cloudXAd),
    CloudXArbiterBid.levelPlay(
        networkName = levelPlayAdInfo.adNetwork,
        revenue = levelPlayAdInfo.revenue,
        precision = levelPlayAdInfo.precision,
    ),
    CloudXArbiterBid.pubmatic(
        price = pobBid.price,
        partnerName = pobBid.partnerName,
    )
)

val configuration = CloudXArbiterConfiguration.builder(bids).build()

CloudX.arbiter(configuration, object : CloudXArbiterListener {
    override fun onCompleted(result: CloudXArbiterResult) {
        Log.d("CloudX", "选中的平台: ${result.platform.name}")
    }
})

CloudXArbiterBid.cloudX() 接收 CloudX 加载回调中的 CloudXAd 对象。levelPlay() 接收 Unity LevelPlay 广告信息值。pubmatic() 接收 PubMatic OpenWrap 出价价格和可选的合作伙伴名称。出价工厂方法中的 partnerNameextras 映射均为可选参数。onCompleted() 在主线程上回调,因此你可以直接在其中展示广告或更新 UI。

result.platform 在选中平台时为 CloudXArbiterPlatform.CLOUDXLEVELPLAYPUBMATIC;当无法选出获胜平台时(例如未传入任何出价),则为 CloudXArbiterPlatform.NONE

AdMob 与 Google Ad Manager

CloudX 会将已加载的 CloudX 广告与已加载的 AdMob 或 Google Ad Manager 广告进行比较。AdMob 和 Ad Manager 是两个独立的需求来源,因此二者可以同时参与同一次仲裁。此方式适用于发布商自行管理的聚合配置,包括商业上称为 AdMob Pro 的账户;SDK 不提供单独的 AdMob Pro API。

Google 需求方通常不会在展示前透露已加载广告的价格,因此仲裁器无法获得可与 CloudX 出价比较的 pre-bid 价格。CloudX 会根据同类广告单元过往的表现估算出价,因此你无需自行提供价格。不需要任何 pre-bid 定价 API。

如果你的 AdMob 账户能够在 pre-bid 阶段提供展示级收益数据,也可以自行提供该精确价格来代替使用估算值——参见下方使用 pre-bid ILRD 手动输入价格

将 Google 付费事件回传给 CloudX(必需)

转发 Google 的付费事件是 Trusted Arbiter 的 AdMob 与 Ad Manager 集成中的必需环节,而不是可选的分析增强项。在展示通过仲裁胜出的 AdMob 或 Ad Manager 广告之后,必须把 Google 为该广告上报的展示级收入交给 CloudX SDK。如果缺少这一步,CloudX 就无法得知 Google 需求方实际支付的价格,后续仲裁的估算质量也会随之下降。

Google 只通过每个广告的 OnPaidEventListener 下发这类收入数据——不存在全局展示事件总线——因此请为每个参与仲裁的 Google 广告设置该监听器,并通过 CloudX.reportRevenueData() 转发每个事件。AdValue.valueMicros 以所报告货币的微单位表示,需要除以 1_000_000.0 才能得到单次展示的收益。AdMob 广告请传入 CloudXRevenuePlatform.ADMOB,Ad Manager 广告请传入 CloudXRevenuePlatform.GAM

private fun Int.toCloudXRevenuePrecision(): CloudXRevenuePrecision = when (this) {
    AdValue.PrecisionType.PRECISE -> CloudXRevenuePrecision.EXACT
    AdValue.PrecisionType.ESTIMATED -> CloudXRevenuePrecision.ESTIMATED
    AdValue.PrecisionType.PUBLISHER_PROVIDED -> CloudXRevenuePrecision.PUBLISHER_DEFINED
    else -> CloudXRevenuePrecision.UNDEFINED
}

private fun reportGooglePaidEvent(
    platform: CloudXRevenuePlatform,
    adValue: AdValue,
    adFormat: String,
    adUnitId: String,
    responseInfo: ResponseInfo?,
): Boolean {
    val servedBy = responseInfo?.loadedAdapterResponseInfo

    return CloudX.reportRevenueData(
        CloudXRevenueData.builder(
            platform = platform,
            revenue = adValue.valueMicros / 1_000_000.0,
            adFormat = adFormat,
        )
            .currencyCode(adValue.currencyCode)
            .precision(adValue.precisionType.toCloudXRevenuePrecision())
            .networkName(servedBy?.adSourceName)
            .adUnitId(adUnitId)
            .thirdPartyAdPlacementId(servedBy?.adSourceInstanceName)
            .build(),
    )
}

// 在展示参与仲裁的 Google 广告之前,为它设置监听器。
adMobInterstitial.setOnPaidEventListener { adValue ->
    reportGooglePaidEvent(
        platform = CloudXRevenuePlatform.ADMOB,
        adValue = adValue,
        adFormat = "interstitial",
        adUnitId = adMobAdUnitId,
        responseInfo = adMobInterstitial.responseInfo,
    )
}

// Ad Manager 广告的上报方式相同,只需改用 GAM 平台。
adManagerInterstitial.setOnPaidEventListener { adValue ->
    reportGooglePaidEvent(
        platform = CloudXRevenuePlatform.GAM,
        adValue = adValue,
        adFormat = "interstitial",
        adUnitId = adManagerAdUnitId,
        responseInfo = adManagerInterstitial.responseInfo,
    )
}

当事件被接受进入 CloudX 收入管道时,reportRevenueData() 返回 true;如果 SDK 尚未初始化或服务端收入追踪已关闭,则返回 false。对于横幅广告和 MREC,只需为 AdView 设置一次监听器——Google 会在同一个视图上重复触发付费事件——并将格式传为 "banner""mrec"。完整的字段说明以及其他受支持的平台参见发布商上报收入数据

用你已加载广告的广告单元 ID 创建出价:

// networkName 为可选参数;如果你知道获胜的广告来源,请传入它,
// 例如 responseInfo?.loadedAdapterResponseInfo?.adSourceName。
val adMobBid = CloudXArbiterBid.adMob(
    adUnitId = adMobAdUnitId,
    networkName = adMobNetworkName ?: "admob",
    manualRevenuePerImpressionUSD = null,
    extras = emptyMap(),
)

// Ad Manager 广告单元 ID 的格式为 /NNNNNNN/placement/name。
val adManagerBid = CloudXArbiterBid.gam(
    adUnitId = "/21775744923/example/interstitial",
    networkName = "gam",
    manualRevenuePerImpressionUSD = null,
    extras = emptyMap(),
)

val configuration = CloudXArbiterConfiguration.builder(
    listOf(CloudXArbiterBid.cloudX(cloudXAd), adMobBid, adManagerBid)
).build()

CloudX.arbiter(configuration, object : CloudXArbiterListener {
    override fun onCompleted(result: CloudXArbiterResult) {
        when (result.platform) {
            CloudXArbiterPlatform.CLOUDX -> cloudXInterstitial.show(activity)
            CloudXArbiterPlatform.ADMOB -> adMobInterstitial.show(activity)
            CloudXArbiterPlatform.GAM -> adManagerInterstitial.show(activity)
            else -> {}
        }
    }
})

获胜的 Google 出价会报告自己的平台——CloudXArbiterPlatform.ADMOBCloudXArbiterPlatform.GAM。你不再需要像处理自定义出价那样,通过 result.platformName 来区分这两个来源。

传入空白的广告单元 ID,出价对象仍会正常构建,不会导致应用崩溃,但它不具备可用的身份信息:既不会被定价,也会被服务器拒绝。

使用 pre-bid ILRD 手动输入价格

部分 AdMob 账户可以在 pre-bid 阶段提供展示级收益数据(ILRD):已加载广告的广告价值在加载时即可获取,早于广告展示。这是一项受账户控制的历史能力,请与你的 Google 客户团队确认你的账户是否已启用。在展示之前就已知的精确单次展示价格,是自行提供价格优于 CloudX 估算值的唯一场景。

将 pre-bid 广告价值作为 manualRevenuePerImpressionUSD 传入,它会覆盖估算值:

val adMobBid = CloudXArbiterBid.adMob(
    adUnitId = adMobAdUnitId,
    networkName = adMobNetworkName ?: "admob",
    // valueMicros 以微单位(micros)表示:1,000,000 微单位等于 1 个货币单位。
    // 请勿再额外除以 1,000——该数值本身已是单次展示的收益,而不是 CPM。
    manualRevenuePerImpressionUSD = preBidAdValue.valueMicros / 1_000_000.0,
    extras = emptyMap(),
)

该数值的处理方式:

  • 0.0 是一个真实的价格。它表示该出价价值为零——而不是价格缺失。
  • 负值和非有限值不是有效价格,因此会被视为缺失并记录日志。
  • 空白的广告单元 ID 会导致手动价格被完全丢弃,因为没有身份信息的出价无法通过校验。

manualRevenuePerImpressionUSD 是单次展示的美元收益,而不是 CPM。请换算你的价格来源报告的数值:

  • 以微单位(micros)表示的数值 必须除以 1,000,000。AdMob 在 Android 上以微单位报告广告价值,1,000,000 微单位等于 1 个货币单位,因此 AdValue.getValueMicros()5000 时对应单次展示 0.005。请勿把微单位数值误当作 eCPM 而再额外除以 1,000。
  • 非美元金额 必须先换算为美元。

分步示例:在 CloudX 与 LevelPlay 之间仲裁

本演练展示了应读取哪个 Unity LevelPlay 回调,以及应将哪些值传入 CloudX.arbiter()。示例使用插屏广告,但相同的字段映射适用于任何广告格式——横幅广告和 MREC 有何不同请参见支持的广告格式

加载两个候选项

创建 CloudX 和 LevelPlay 插屏广告,绑定监听器,并为每个平台启动加载。

val cloudXInterstitial = CloudX.createInterstitial(context, "YOUR_CLOUDX_AD_UNIT_ID")
cloudXInterstitial.listener = cloudXListener
cloudXInterstitial.load()

val levelPlayInterstitial = LevelPlayInterstitialAd("YOUR_LEVELPLAY_AD_UNIT_ID")
levelPlayInterstitial.setListener(levelPlayListener)
levelPlayInterstitial.loadAd()

保存每个平台已加载的广告

LevelPlay 在其 onAdLoaded 回调中提供 LevelPlayAdInfo;CloudX 在 onAdLoaded 中提供 CloudXAd。请保存两者——下一步会从中读取仲裁输入。

private var cloudXAd: CloudXAd? = null
private var levelPlayInfo: LevelPlayAdInfo? = null

// CloudXInterstitialListener
override fun onAdLoaded(cloudXAd: CloudXAd) {
    this.cloudXAd = cloudXAd
}

// LevelPlayInterstitialAdListener
override fun onAdLoaded(levelPlayAdInfo: LevelPlayAdInfo) {
    levelPlayInfo = levelPlayAdInfo
}

将值映射为出价

LevelPlayAdInfo 读取 LevelPlay 字段,并传给 CloudXArbiterBid.levelPlay()。CloudX 出价直接接收 CloudXAdlistOfNotNull 只会提交实际加载成功的平台。

LevelPlayAdInfo 字段类型CloudXArbiterBid.levelPlay 参数
adNetworkStringnetworkName
revenueDoublerevenue
precisionStringprecision
val bids = listOfNotNull(
    cloudXAd?.let { CloudXArbiterBid.cloudX(it) },
    levelPlayInfo?.let { info ->
        CloudXArbiterBid.levelPlay(
            networkName = info.adNetwork,   // LevelPlayAdInfo.adNetwork
            revenue = info.revenue,         // LevelPlayAdInfo.revenue
            precision = info.precision,     // LevelPlayAdInfo.precision
        )
    },
)

运行仲裁器

将出价封装进 CloudXArbiterConfiguration,连同监听器传给 CloudX.arbiter()onCompleted() 在主线程上回调。

val configuration = CloudXArbiterConfiguration.builder(bids).build()
CloudX.arbiter(configuration, object : CloudXArbiterListener {
    override fun onCompleted(result: CloudXArbiterResult) {
        showWinner(result)
    }
})

展示获胜平台

根据 result.platform 进行分支,展示获胜平台的广告。CloudXArbiterPlatform.NONE 表示未选出获胜平台——此时不展示广告,继续应用流程。

private fun showWinner(result: CloudXArbiterResult) {
    when (result.platform) {
        CloudXArbiterPlatform.CLOUDX -> cloudXInterstitial.show(activity)
        CloudXArbiterPlatform.LEVELPLAY -> levelPlayInterstitial.showAd(activity)
        else -> { } // CloudXArbiterPlatform.NONE——无获胜平台;不展示广告
    }
}

下方的 ArbiterInterstitialController 将上述步骤封装为一个可复用的组件,会在到达广告位之前提前准备好获胜平台。

插屏广告示例

这个插屏广告示例会在两个平台之间仲裁:CloudX 和 Unity LevelPlay。在到达广告位之前先准备好获胜平台:

  1. 并行加载 CloudX 和 LevelPlay。
  2. 等待两个平台都加载完成或加载失败。
  3. 只将已加载的候选项提交给 Trusted Arbiter。
  4. 缓存选中的平台。
  5. 到达广告位时,立即展示缓存的获胜广告。

如果两个平台都加载失败,开始新的加载周期。如果到达广告位时还没有准备好获胜平台,则继续应用流程,不展示广告。

ArbiterInterstitialController.kt
/**
 * 提前准备好 Trusted Arbiter 的获胜平台,以便在到达广告位时立即展示插屏广告。
 *
 * 并行加载 [cloudXInterstitial] 和 [levelPlayInterstitial],等待两者都加载完成或加载失败后,
 * 将已加载的候选项提交给 [CloudX.arbiter],并将选中的 [CloudXArbiterPlatform] 缓存到
 * [nextWinner] 中。
 */
class ArbiterInterstitialController(
    private val cloudXInterstitial: CloudXInterstitialAd,
    private val levelPlayInterstitial: LevelPlayInterstitialAd,
) {
    /** 在获胜平台准备就绪后接收仲裁结果。 */
    interface Listener {
        /** 当仲裁器为下一次展示选定 [platform] 时调用。 */
        fun onWinnerPrepared(platform: CloudXArbiterPlatform)
    }

    /** 设置该属性以监听 [Listener.onWinnerPrepared] 回调。 */
    var listener: Listener? = null
    private var cloudXAd: CloudXAd? = null
    private var cloudXLoadDone = false
    private var levelPlayAdInfo: LevelPlayAdInfo? = null
    private var levelPlayLoadDone = false
    private var nextWinner: CloudXArbiterPlatform? = null

    init {
        cloudXInterstitial.listener = createCloudXListener()
        levelPlayInterstitial.setListener(createLevelPlayListener())
    }

    /** 为当前没有缓存广告的每个平台启动加载。 */
    fun loadMissingAds() {
        if (cloudXAd == null) cloudXInterstitial.load()
        if (levelPlayAdInfo == null) levelPlayInterstitial.loadAd()
    }

    /**
     * 在 [activity] 上展示已准备好的获胜广告,仅在实际调用展示方法时返回 true。
     *
     * 当没有就绪的获胜平台,或缓存的广告已不再可用时返回 false,此时会启动新的加载周期。
     */
    fun showAtPlacement(activity: Activity): Boolean {
        return when (nextWinner) {
            CloudXArbiterPlatform.CLOUDX -> showCloudX(activity)
            CloudXArbiterPlatform.LEVELPLAY -> showLevelPlay(activity)
            else -> false
        }
    }

    /**
     * 在两个平台都完成加载后运行仲裁器,然后缓存获胜平台。
     *
     * 在两个加载都完成之前会提前返回。如果两个平台都未加载成功,则重新启动加载周期;
     * 否则将已加载的候选项提交给 [CloudX.arbiter]。
     */
    private fun maybePrepareWinner() {
        if (!cloudXLoadDone || !levelPlayLoadDone) return

        if (cloudXAd == null && levelPlayAdInfo == null) {
            cloudXLoadDone = false
            levelPlayLoadDone = false
            loadMissingAds()
            return
        }

        val bids = listOfNotNull(
            cloudXAd?.let { CloudXArbiterBid.cloudX(it) },
            levelPlayAdInfo?.let { adInfo ->
                CloudXArbiterBid.levelPlay(
                    networkName = adInfo.adNetwork,
                    revenue = adInfo.revenue,
                    precision = adInfo.precision,
                )
            }
        )

        CloudX.arbiter(
            configuration = CloudXArbiterConfiguration.builder(bids).build(),
            listener = object : CloudXArbiterListener {
                override fun onCompleted(result: CloudXArbiterResult) {
                    nextWinner = result.platform
                    listener?.onWinnerPrepared(result.platform)
                }
            }
        )
    }

    private fun showCloudX(activity: Activity): Boolean {
        if (cloudXInterstitial.isAdReady) {
            cloudXInterstitial.show(activity)
            return true
        }

        clearCloudXAndLoadMissingAds()
        return false
    }

    private fun showLevelPlay(activity: Activity): Boolean {
        if (levelPlayInterstitial.isAdReady) {
            levelPlayInterstitial.showAd(activity)
            return true
        }

        clearLevelPlayAndLoadMissingAds()
        return false
    }

    private fun clearCloudXAndLoadMissingAds() {
        cloudXAd = null
        cloudXLoadDone = false
        nextWinner = null
        loadMissingAds()
    }

    private fun clearLevelPlayAndLoadMissingAds() {
        levelPlayAdInfo = null
        levelPlayLoadDone = false
        nextWinner = null
        loadMissingAds()
    }

    private fun createCloudXListener() = object : CloudXInterstitialListener {
        override fun onAdLoaded(cloudXAd: CloudXAd) {
            this@ArbiterInterstitialController.cloudXAd = cloudXAd
            cloudXLoadDone = true
            maybePrepareWinner()
        }

        override fun onAdLoadFailed(adUnitId: String, cloudXError: CloudXError) {
            cloudXAd = null
            cloudXLoadDone = true
            maybePrepareWinner()
        }

        override fun onAdDisplayed(cloudXAd: CloudXAd) = Unit

        override fun onAdDisplayFailed(cloudXAd: CloudXAd, cloudXError: CloudXError) {
            clearCloudXAndLoadMissingAds()
        }

        override fun onAdHidden(cloudXAd: CloudXAd) {
            clearCloudXAndLoadMissingAds()
        }

        override fun onAdClicked(cloudXAd: CloudXAd) = Unit
    }

    private fun createLevelPlayListener() = object : LevelPlayInterstitialAdListener {
        override fun onAdLoaded(levelPlayAdInfo: LevelPlayAdInfo) {
            this@ArbiterInterstitialController.levelPlayAdInfo = levelPlayAdInfo
            levelPlayLoadDone = true
            maybePrepareWinner()
        }

        override fun onAdLoadFailed(levelPlayAdError: LevelPlayAdError) {
            levelPlayAdInfo = null
            levelPlayLoadDone = true
            maybePrepareWinner()
        }

        override fun onAdInfoChanged(levelPlayAdInfo: LevelPlayAdInfo) {
            this@ArbiterInterstitialController.levelPlayAdInfo = levelPlayAdInfo
        }

        override fun onAdDisplayed(levelPlayAdInfo: LevelPlayAdInfo) = Unit

        override fun onAdDisplayFailed(
            levelPlayAdError: LevelPlayAdError,
            levelPlayAdInfo: LevelPlayAdInfo
        ) {
            clearLevelPlayAndLoadMissingAds()
        }

        override fun onAdClosed(levelPlayAdInfo: LevelPlayAdInfo) {
            clearLevelPlayAndLoadMissingAds()
        }

        override fun onAdClicked(levelPlayAdInfo: LevelPlayAdInfo) = Unit
    }
}

showAtPlacement() 只有在实际调用广告展示方法时才会返回 trueonAdInfoChanged() 会在 LevelPlay 广告仍然处于已加载状态时,保持缓存的 LevelPlay 候选项为最新。

对于 PubMatic OpenWrap,使用 CloudXArbiterBid.pubmatic(price, partnerName) 创建第三方出价。如果仲裁服务不可用,SDK 会在传入的受支持出价中回退选择可比较美元出价最高的平台。

横幅与 MREC 仲裁

横幅广告和 MREC 属于视图格式:多个网络可以同时持有已加载的广告,但屏幕上只能展示一个视图。相较于全屏格式,横幅和 MREC 的仲裁多出三项要求。

禁用自动刷新

必须在各处都关闭自动刷新,让仲裁器(而不是各网络内部的计时器)来控制何时展示新广告:

  • 在 CloudX 控制台中为该广告单元关闭自动刷新。
  • 创建 CloudXAdView 后立即调用 stopAutoRefresh()
  • 同时在其他参与仲裁的网络的横幅广告 API 上禁用自动刷新。

视图附加

所有候选广告都应在屏幕外加载。只有获胜出价对应的横幅视图可以被添加到视图层级或展示。落选的视图应保持未附加状态——不要将其添加到任何父视图,哪怕只是临时添加——因为添加视图会使其渲染,并可能为仲裁器拒绝的出价触发展示。与标准横幅集成不同(后者在创建时就将视图添加到布局中,参见 横幅与 MREC),使用 Trusted Arbiter 时视图不得在创建时附加——只有在仲裁选中其为获胜方后才能附加。

刷新周期

禁用自动刷新后,仲裁器必须驱动自己的刷新循环:

运行第一轮

并行加载所有网络的候选广告,运行仲裁器,并附加获胜视图。

为获胜方启动下一次加载

获胜出价的展示一旦触发,立即为获胜网络启动新一轮加载,以便为下一轮做好准备。

保留未获胜的已填充广告

保留未获胜网络已经填充的广告,留待下一轮仲裁使用。只对上一轮未填充的网络重新发起加载请求。

重新仲裁

等待未完成的加载响应全部返回后,针对完整的候选集合再次运行仲裁器。

按周期切换

每隔 20-30 秒刷新一次展示中的广告,将新的获胜视图替换掉旧视图。间隔短于 20 秒会降低 CPM 表现。

横幅广告示例

以下示例在 CloudX 横幅广告与 LevelPlay 横幅广告之间进行仲裁,并驱动上述刷新周期。MREC 的结构相同——将 CloudX.createBanner() 替换为 CloudX.createMREC(),并将 LevelPlay 横幅类型替换为 LevelPlay MREC 类型即可。

ArbiterBannerController.kt
/**
 * 为横幅广告位驱动 Trusted Arbiter:并行加载 [cloudXBanner] 和 [levelPlayBanner],
 * 在已填充的候选项之间进行仲裁,将获胜视图添加到 [container],并按
 * [refreshIntervalMs] 周期性刷新。
 *
 * 必须在两个网络上都禁用自动刷新(CloudX 通过控制台设置,此处通过
 * `stopAutoRefresh()`,LevelPlay 通过其对应设置)——刷新周期改由本控制器接管。
 */
class ArbiterBannerController(
    private val cloudXBanner: CloudXAdView,
    private val levelPlayBanner: LevelPlayBannerAdView,
    private val container: ViewGroup,
    private val refreshIntervalMs: Long = 25_000L,
) {
    private val handler = Handler(Looper.getMainLooper())
    private var cloudXAd: CloudXAd? = null
    private var cloudXFilled = false
    private var cloudXLoadDone = false
    private var levelPlayAdInfo: LevelPlayAdInfo? = null
    private var levelPlayFilled = false
    private var levelPlayLoadDone = false
    private var currentWinner: CloudXArbiterPlatform? = null

    private val refreshRunnable = Runnable { runArbitrationRound() }

    init {
        cloudXBanner.stopAutoRefresh()
        cloudXBanner.listener = createCloudXListener()
        cloudXBanner.revenueListener = createCloudXRevenueListener()
        levelPlayBanner.bannerListener = createLevelPlayListener()
    }

    /** 启动第一轮:为尚未持有填充广告的每个网络发起加载。 */
    fun start() {
        loadMissingAds()
    }

    fun stop() {
        handler.removeCallbacks(refreshRunnable)
    }

    private fun loadMissingAds() {
        if (!cloudXFilled) cloudXBanner.load()
        if (!levelPlayFilled) levelPlayBanner.loadAd()
    }

    /** 仅在两个网络都已结束加载(无论填充还是失败)时运行;只针对待处理的候选项重新仲裁。 */
    private fun maybeArbitrate() {
        if (!cloudXLoadDone || !levelPlayLoadDone) return

        val bids = listOfNotNull(
            cloudXAd?.let { CloudXArbiterBid.cloudX(it) },
            levelPlayAdInfo?.let { info ->
                CloudXArbiterBid.levelPlay(
                    networkName = info.adNetwork,
                    revenue = info.revenue,
                    precision = info.precision,
                )
            },
        )

        if (bids.isEmpty()) {
            loadMissingAds()
            return
        }

        CloudX.arbiter(
            configuration = CloudXArbiterConfiguration.builder(bids).build(),
            listener = object : CloudXArbiterListener {
                override fun onCompleted(result: CloudXArbiterResult) {
                    showWinner(result.platform)
                }
            }
        )
    }

    private fun runArbitrationRound() {
        maybeArbitrate()
    }

    /** 只附加获胜视图;落选视图永远不会被添加到 [container] 中。 */
    private fun showWinner(platform: CloudXArbiterPlatform) {
        currentWinner = platform
        container.removeAllViews()

        when (platform) {
            CloudXArbiterPlatform.CLOUDX -> container.addView(cloudXBanner)
            CloudXArbiterPlatform.LEVELPLAY -> container.addView(levelPlayBanner)
            else -> return // CloudXArbiterPlatform.NONE——无获胜平台;container 保持为空
        }

        handler.removeCallbacks(refreshRunnable)
        handler.postDelayed(refreshRunnable, refreshIntervalMs)
    }

    /** 获胜网络的展示一旦触发,立即启动其下一次加载。 */
    private fun onWinnerImpression(platform: CloudXArbiterPlatform) {
        when (platform) {
            CloudXArbiterPlatform.CLOUDX -> {
                cloudXFilled = false
                cloudXLoadDone = false
                cloudXBanner.load()
            }
            CloudXArbiterPlatform.LEVELPLAY -> {
                levelPlayFilled = false
                levelPlayLoadDone = false
                levelPlayBanner.loadAd()
            }
            else -> Unit
        }
    }

    private fun createCloudXListener() = object : CloudXAdViewListener {
        override fun onAdLoaded(cloudXAd: CloudXAd) {
            this@ArbiterBannerController.cloudXAd = cloudXAd
            cloudXFilled = true
            cloudXLoadDone = true
            maybeArbitrate()
        }

        override fun onAdLoadFailed(adUnitId: String, cloudXError: CloudXError) {
            cloudXAd = null
            cloudXFilled = false
            cloudXLoadDone = true
            maybeArbitrate()
        }

        override fun onAdClicked(cloudXAd: CloudXAd) = Unit
        override fun onAdExpanded(cloudXAd: CloudXAd) = Unit
        override fun onAdCollapsed(cloudXAd: CloudXAd) = Unit
    }

    private fun createCloudXRevenueListener() = object : CloudXAdRevenueListener {
        override fun onAdRevenuePaid(cloudXAd: CloudXAd) {
            // onAdRevenuePaid 在 CloudX 展示发生时触发;仅当 CloudX 正在展示时才响应。
            if (currentWinner == CloudXArbiterPlatform.CLOUDX) {
                onWinnerImpression(CloudXArbiterPlatform.CLOUDX)
            }
        }
    }

    private fun createLevelPlayListener() = object : LevelPlayBannerAdViewListener {
        override fun onAdLoaded(levelPlayAdInfo: LevelPlayAdInfo) {
            this@ArbiterBannerController.levelPlayAdInfo = levelPlayAdInfo
            levelPlayFilled = true
            levelPlayLoadDone = true
            maybeArbitrate()
        }

        override fun onAdLoadFailed(levelPlayAdError: LevelPlayAdError) {
            levelPlayAdInfo = null
            levelPlayFilled = false
            levelPlayLoadDone = true
            maybeArbitrate()
        }

        override fun onAdDisplayed(levelPlayAdInfo: LevelPlayAdInfo) {
            if (currentWinner == CloudXArbiterPlatform.LEVELPLAY) {
                onWinnerImpression(CloudXArbiterPlatform.LEVELPLAY)
            }
        }

        override fun onAdClicked(levelPlayAdInfo: LevelPlayAdInfo) = Unit
    }
}

onAdRevenuePaid()(CloudX)和 onAdDisplayed()(LevelPlay)分别是各自网络展示发生的位置;获胜网络的下一次加载正是在这里启动,从而在不影响当前展示的情况下为下一轮准备好新的填充广告。已经持有已填充、未附加广告的网络会直接跳过 loadMissingAds(),直到该填充被使用或过期。

自定义出价输入

当需要让 Trusted Arbiter 比较 CloudX 与没有专用出价辅助方法的第三方平台时,可以使用 CloudXArbiterBid.custom()

val customBid = CloudXArbiterBid.custom(
    platformName = "my_mediation_platform",
    networkName = "winning_demand_source",
    revenuePerImpressionUSD = 0.00125,
    precision = CloudXArbiterPrecision.EXACT,
    extras = mapOf("ad_unit" to "third-party-ad-unit-id"),
)

val configuration = CloudXArbiterConfiguration.builder(
    listOf(CloudXArbiterBid.cloudX(cloudXAd), customBid)
).build()

当自定义出价胜出时,result.platformCloudXArbiterPlatform.CUSTOMresult.platformName 为出价中传入的 platformNamerevenuePerImpressionUSD 应传入单次展示的美元收益,而不是 CPM。请使用 CloudXArbiterPrecision.EXACTESTIMATEDPUBLISHER_DEFINEDUNDEFINED 描述该收益值的精度。