**激励视频广告** [激励视频广告介绍](https://uniapp.dcloud.net.cn/component/ad-rewarded-video.html) **平台差异说明** |App|H5|微信小程序|支付宝小程序|百度小程序|字节跳动小程序|飞书小程序|QQ小程序|快手小程序|京东小程序| |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:| |√(2.5.11+)|x|√|x|x|√(1.57.0+)|x|√(0.1.26+)|√|x| **开通配置广告** [开通广告步骤详情](https://uniapp.dcloud.net.cn/uni-ad.html#start) 激励视频广告组件是原生组件,层级最高,会覆盖在普通前端组件上。 ### 语法 `uni.createRewardedVideoAd(options)` ### 参数说明 options 为 object 类型,属性如下: |属性名 |类型 |必填 |描述 |最低支持版本 | |:-:|:-:|:-:|:-:|:-:| |adpid |string | 是|广告位 id |App 2.5.11+| |adUnitId |string | 是|广告位 id |微信小程序2.6.0+, QQ0.1.26+, 字节跳动1.57.0+| ### 返回值 返回值为 RewarededVideoAd 类型,属性如下: |属性名|类型|描述|最低支持版本| |:-:|:-:|:-:|:-:| |show|Function|广告创建后默认是隐藏的,可以通过该方法显示广告|App 2.5.11+, 微信小程序2.6.0+, QQ0.1.26+,字节跳动1.57.0+| |onLoad|Function|绑定广告 load 事件的监听器|App 2.5.11+, 微信小程序2.6.0+, QQ0.1.26+,字节跳动1.57.0+| |offLoad|Function|解除绑定 load 事件的监听器|QQ0.1.26+,字节跳动1.57.0+| |load|Function|当广告素材加载出现错误时,可以通过 load 方法手动加载|App 2.5.11+, 微信小程序2.6.0+, QQ0.1.26+,字节跳动1.57.0+| |onError|Function|绑定 error 事件的监听器 |App 2.5.11+, 微信小程序2.6.0+, QQ0.1.26+,字节跳动1.57.0+| |onAdClicked|Function|绑定广告可点击屏幕区域事件的监听器 |App 2.5.11+| |offError|Function|解除绑定 error 事件的监听器|QQ0.1.26+,字节跳动1.57.0+| |onClose|Function|绑定 close 事件的监听器|App 2.5.11+, 微信小程序2.6.0+, QQ0.1.26+,字节跳动1.57.0+| |offClose|Function|解除绑定 close 事件的监听器|QQ0.1.26+,字节跳动1.57.0+| ### 广告创建 开发者可以调用 `uni.createRewardedVideoAd` 创建激励视频广告组件。 激励视频广告组件默认是隐藏的,因此可以提前创建,以提前初始化组件。开发者可以在页面的 `onReady` 事件回调中创建广告实例,并在该页面的生命周期内重复调用该广告实例。 ```html ``` ### 推荐接入示例@ad-js `ad.js` 是对 `uni.createRewardedVideoAd` 的封装,一个页面缓存多页面生效,避免每个页面都预载而不展示的问题,可以传入不同广告位,内部处理了Loading状态、快速点击、数据过期、失败重试1次逻辑 ```html ``` ```js // ad.js const ADType = { RewardedVideo: "RewardedVideo", FullScreenVideo: "FullScreenVideo" } class AdHelper { constructor() { this._ads = {} } load(options, onload, onerror) { let ops = this._fixOldOptions(options) let { adpid } = ops if (!adpid || this.isBusy(adpid)) { return } this.get(ops).load(onload, onerror) } show(options, onsuccess, onfail) { let ops = this._fixOldOptions(options) let { adpid } = ops if (!adpid) { return } uni.showLoading({ mask: true }) var ad = this.get(ops) ad.load(() => { uni.hideLoading() ad.show((e) => { onsuccess && onsuccess(e) }) }, (err) => { uni.hideLoading() onfail && onfail(err) }) } isBusy(adpid) { return (this._ads[adpid] && this._ads[adpid].isLoading) } get(options) { const { adpid, singleton = true } = options if (singleton === false) { if (this._ads[adpid]) { this._ads[adpid].destroy() delete this._ads[adpid] } } delete options.singleton if (!this._ads[adpid]) { this._ads[adpid] = this._createAdInstance(options) } return this._ads[adpid] } _createAdInstance(options) { const adType = options.adType || ADType.RewardedVideo delete options.adType let ad = null; if (adType === ADType.RewardedVideo) { ad = new RewardedVideo(options) } else if (adType === ADType.FullScreenVideo) { ad = new FullScreenVideo(options) } return ad } _fixOldOptions(options) { return (typeof options === "string") ? { adpid: options } : options } } const EXPIRED_TIME = 1000 * 60 * 30 const ProviderType = { CSJ: 'csj', GDT: 'gdt' } const RETRY_COUNT = 1 class AdBase { constructor(adInstance, options = {}) { this._isLoad = false this._isLoading = false this._lastLoadTime = 0 this._lastError = null this._retryCount = 0 this._loadCallback = null this._closeCallback = null this._errorCallback = null const ad = this._ad = adInstance ad.onLoad((e) => { this._isLoading = false this._isLoad = true this._lastLoadTime = Date.now() this.onLoad() }) ad.onClose((e) => { this._isLoad = false this.onClose(e) }) ad.onVerify && ad.onVerify((e) => { // e.isValid }) ad.onError(({ code, message }) => { this._isLoading = false const data = { code: code, errMsg: message } if (code === -5008) { this._loadAd() return } if (this._retryCount < RETRY_COUNT) { this._retryCount += 1 this._loadAd() return } this._lastError = data this.onError(data) }) } get isExpired() { return (this._lastLoadTime !== 0 && (Math.abs(Date.now() - this._lastLoadTime) > EXPIRED_TIME)) } get isLoading() { return this._isLoading } getProvider() { return this._ad.getProvider() } load(onload, onerror) { this._loadCallback = onload this._errorCallback = onerror if (this._isLoading) { return } if (this._isLoad) { this.onLoad() return } this._retryCount = 0 this._loadAd() } show(onclose) { this._closeCallback = onclose if (this._isLoading || !this._isLoad) { return } if (this._lastError !== null) { this.onError(this._lastError) return } const provider = this.getProvider() if (provider === ProviderType.CSJ && this.isExpired) { this._loadAd() return } this._ad.show() } onLoad(e) { if (this._loadCallback != null) { this._loadCallback() } } onClose(e) { if (this._closeCallback != null) { this._closeCallback({ isEnded: e.isEnded }) } } onError(e) { if (this._errorCallback != null) { this._errorCallback(e) } } destroy() { this._ad.destroy() } _loadAd() { this._isLoad = false this._isLoading = true this._lastError = null this._ad.load() } } class RewardedVideo extends AdBase { constructor(options = {}) { super(plus.ad.createRewardedVideoAd(options), options) } } class FullScreenVideo extends AdBase { constructor(options = {}) { super(plus.ad.createFullScreenVideoAd(options), options) } } export default new AdHelper() ``` ### 显示/隐藏 激励视频广告组件默认是隐藏的,在用户主动触发广告后,开发者需要调用 RewardedVideoAd.show() 进行显示。 ```js rewardedVideoAd.show() ``` 只有在用户点击激励视频广告组件上的 关闭广告 按钮时,广告才会关闭。开发者不可控制激励视频广告组件的隐藏。 ### 广告拉取成功与失败 激励视频广告组件是自动拉取广告并进行更新的。在组件创建后会拉取一次广告,用户点击 关闭广告 后会去拉取下一条广告。 如果拉取成功,通过 `RewardedVideoAd.onLoad()` 注册的回调函数会执行,`RewardedVideoAd.show()` 返回的 Promise 也会是一个 resolved Promise。两者的回调函数中都没有参数传递。 ```js rewardedVideoAd.onLoad(() => { console.log('激励视频 广告加载成功') }) rewardedVideoAd.show() .then(() => console.log('激励视频 广告显示')) ``` 如果拉取失败,通过 `RewardedVideoAd.onError()` 注册的回调函数会执行,回调函数的参数是一个包含错误信息的对象。常见异常错误参考文档 ```js rewardedVideoAd.onError(err => { console.log(err) }) ``` `RewardedVideoAd.show()` 返回的 Promise 也会是一个 rejected Promise。 ```js rewardedVideoAd.show() .catch(err => console.log(err)) ``` ### 拉取失败,重新拉取 如果组件的某次自动拉取失败,那么之后调用的 show() 将会被 reject。此时可以调用 `RewardedVideoAd.load()` 手动重新拉取广告。 ```js rewardedVideoAd.show() .catch(() => { rewardedVideoAd.load() .then(() => rewardedVideoAd.show()) .catch(err => { console.log('激励视频 广告显示失败') }) }) ``` 如果组件的自动拉取是成功的,那么调用 `load()` 方法会直接返回一个 resolved Promise,而不会去拉取广告。 ```js rewardedVideoAd.load() .then(() => rewardedVideoAd.show()) ``` ### 监听用户关闭广告 ![](https://bjetxgzv.cdn.bspapp.com/VKCEYUGU-uni-app-doc/24d1db60-441f-11eb-bd01-97bc1429a9ff.png) 只有在用户点击激励视频广告组件上的 关闭广告 按钮时,广告才会关闭。这个事件可以通过 `RewardedVideoAd.onClose()` 监听。 `RewardedVideoAd.onClose()` 的回调函数会传入一个参数 res,res.isEnded 描述广告被关闭时的状态。 |属性|类型|说明| |:-:|:-:|:-:| |isEnded|boolean|视频是否是在用户完整观看的情况下被关闭的,true 表示用户是在视频播放完以后关闭的视频,false 表示用户在视频播放过程中关闭了视频 开发者需要根据 res.isEnded 判断是否视频是否播放结束,如果成功播放完毕则应该向用户发放奖励。 ```js rewardedVideoAd.onClose(res => { // 用户点击了【关闭广告】按钮 if (res && res.isEnded) { // 正常播放结束 // 这里应该联网给予用户激励。且这段代码应该做安全保护,详见下文中的“安全注意” } else { // 播放中途退出 } }) ``` ### 服务器回调@callback App平台 3.1.15+ 支持穿山甲/优量汇/快手 激励视频广告可以支持广告服务器到业务服务器的回调,用于业务系统判断是否提供奖励给观看广告的用户。配置服务器回调后,当用户成功看完广告时,广告服务器会访问配置的云函数,通知用户完成观看激励视频。 相对来讲服务器回调将更加安全,可以依赖广告平台的反作弊机制来避免用户模拟观看广告完成的事件。 ![激励视频回调](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-f184e7c3-1912-41b2-b81f-435d1b37c7b4/d0e94790-68e4-4007-8e34-bdb8cb6b4d34.jpg) 如何使用 1. 申请激励视频广告位时开启服务器回调 2. 创建激励视频广告时传入回调参数 urlCallback示例 ```js rewardedVideoAd = uni.createRewardedVideoAd({ adpid: '', urlCallback: { userId: 'testuser', extra: 'testdata' } }); rewardedVideoAd.onClose(e => { }) ``` ### 服务器回调说明 服务器回调基于[uniCloud](https://uniapp.dcloud.net.cn/uniCloud/README),详细流程如下: 1. 登陆 [uniCloud](https://unicloud.dcloud.net.cn/) web控制台,新建服务空间或选择已有服务空间,然后在HBuilderX中新建uni-app项目并关联服务空间,新建云函数上传,用于接收广告的回调 2. 在 [uniAD](https://uniad.dcloud.net.cn/) web控制台开通服务器回调并选择上一步新建的云函数 3. 开通后将在选择的服务空间下自动部署一个加密云函数 `uniAdCallback` 4. `uniAdCallback` 接收广告商服务器回调验证签名并抹平穿山甲/优量汇/快手参数差异,然后以 [callFunction](https://uniapp.dcloud.net.cn/uniCloud/cf-functions?id=callbyfunction) 方式调用用户云函数 5. 用户在自己的云函数中处理业务 注意: 1. 服务器通信和前端事件是并行的,前端需要轮询向服务器请求并验证结果 2. 不建议在 `uniAD` web控制修改回调的服务空间和云函数名称,因为修改后生效需要一段时间 ### Q&A Q: 回调为什么使用[uniCloud](https://uniapp.dcloud.net.cn/uniCloud/README),而不是直接配置开发者的服务器 A: 1. 由于多家广告商的回调和签名验证逻辑不同,开发者需要写很多逻辑,`uniCloud` 中的云函数 `uniAdCallback` 已抹平了差异,开发者按照统一的参数处理即可 2. 开发者的服务器有可能响应慢或失去响应造成回调数据丢失, 使用 `uniCloud` 可以帮助开发者保存一份来自广告商服务器的回调数据到开发者的云数据中,以便开发者主动查询 3. `uniCloud` 可以承载大并发、防DDoS攻击,无需运营人员维护,如果选择了 `阿里云` 且是免费的 ### 云函数uniAdCallback传递的参数 |字段定义|类型|字段名称|备注| |:-:|:-:|:-:|:-:| |adpid|String|DCloud广告位id|| |provider|String|广告服务商|csj、ks、gdt、sigmob| |platform|String|平台|iOS、Android| |trans_id|String|交易id|完成观看的唯一交易ID| |user_id|String|用户id|调用SDK透传,应用对用户的唯一标识| |extra|String|自定义数据|调用SDK传入并透传,如无需要则为空| #### 用户的云函数返回数据约定 返回json数据,字段如下: 字段名称|说明|字段类型|备注| :-|:-|:-|:-| isValid|校验结果|Blean|判定结果,是否发放奖励| 示例 ```js exports.main = async (event, context) => { //event为客户端上传的参数 console.log('event : ', event); return { "isValid": true } }; ``` #### 用户云函数详细说明 如果业务使用了uniCloud,可以直接在云函数内部处理 也可以将结果发送给已有业务服务器 示例代码 ```js 'use strict'; const crypto = require('crypto'); const db = uniCloud.database(); const DEFAUTL_TIMEOUT = 30000; const DEFAUTL_RETRY_COUNT = 3; const RETRY_TIMEOUT = 3000; const ProviderType = { CSJ: "csj", GDT: "gdt", KS: "ks" }; const collectionName = "opendb-uniad-callback-log"; class DB { static save(data) { return new DB().add(data); } add(data) { const collection = db.collection(collectionName); const data2 = Object.assign(data, { ad_type: 0, create_date: new Date() }) return collection.add(data2); } } class UserServer { static send(url, data) { return new UserServer().sendHttpRequest(url, data); } async sendHttpRequest(url, data) { let needRetry = data.provider !== ProviderType.GDT; let retryCount = needRetry ? DEFAUTL_RETRY_COUNT : 1; let timeout = needRetry ? RETRY_TIMEOUT : DEFAUTL_TIMEOUT; let result = null; while (retryCount > 0) { console.log("sendHttpRequest::count::" + retryCount + "::", url, data); try { result = await uniCloud.httpclient.request(url, { data, dataType: 'json', contentType: 'json', timeout }); if (result.data && result.data.isValid === true) { break; } } catch (e) { console.log(e); } retryCount--; } return result; } } exports.main = async (event, context) => { //event为客户端上传的参数 console.log('event : ', event); const { path, queryStringParameters } = event; const data = { adpid: event.adpid, platform: event.platform, provider: event.provider, trans_id: event.trans_id, sign: event.sign, user_id: event.user_id, extra: event.extra, } // 注意::必须验签请求来源 const secret = "";// uniad 后台开通激励视频回调后生成的 Security key const trans_id = event.trans_id; const sign2 = crypto.createHash('sha256').update(`${secret}:${trans_id}`).digest('hex'); if (event.sign !== sign2) { return null; } // 可选将回调记录保存到uniCloud,避免用户服务器没有响应时有日志可查,如果选择了保存记录需要做定时清理日志,避免日志过多影响性能 // try { // await DB.save(data); // } catch (e) { // console.log(e); // } //const url = "https://"; // 用户业务服务器地址,为了避免请求被伪造,必须使用签名的方式请求 //let reuslt = await UserServer.send(url, data); return reuslt }; ``` #### 安全注意 由于激励视频对应着用户奖励,可能会遇到恶意刷激励奖励但实际上并不看广告的情况。此时广告平台不给结算,但开发者却可能把激励送出去。 为了提升安全性,建议所有使用激励视频的开发者都要做如下工作来加强保护: 1. 前端代码加密。涉及激励相关的,在manifest中配置好要加密的代码文件,打包后会自动加密相应文件。[详见](https://ask.dcloud.net.cn/article/36437) 2. apk加固。即便前端代码加密,原生层引擎的java代码仍然可能被反编译,需要对apk加固。市面上很多加固服务,比如360加固、爱加密加固均可以自行选择。 3. 使用如下安全类API,防止客户端被篡改 - plus.navigator.getSignature 获取应用签名标识。结合在服务器端存放证书信息,可比对判断App的证书是否被重签 [规范](https://www.html5plus.org/doc/zh_cn/navigator.html#plus.navigator.getSignature) - plus.navigator.isSimulator 判断App是否运行在模拟器环境 [规范](https://www.html5plus.org/doc/zh_cn/navigator.html#plus.navigator.isSimulator) - plus.navigator.isRoot 判断设备是否被root或越狱 [规范](https://www.html5plus.org/doc/zh_cn/navigator.html#plus.navigator.isRoot) - plus.networkinfo.isSetProxy 判断设备的网络是否设置了代理 [规范](https://www.html5plus.org/doc/zh_cn/device.html#plus.networkinfo.isSetProxy) 4. 避免使用短信验证码来识别身份,推荐使用可信度更高的 [手机号一键登录](/univerify) 或 [微信登录](/api/plugins/login?id=login) 5. 必要时可使用[生物认证(指纹和faceid)](/api/system/authentication)、[活体检测的sdk](https://ext.dcloud.net.cn/search?q=%E6%B4%BB%E4%BD%93%E6%A3%80%E6%B5%8B&orderBy=Relevance&cat1=5&cat2=51) #### 获取广告商名称 > HBuilderX 2.6.8+ #### 语法 `RewardedVideoAd.getProvider()` #### 说明 返回值 为 string 类型 |值|描述| |:-:|:-:| |csj|穿山甲| |gdt|腾讯优量汇(前称广点通)| |ks|快手| |sigmob|Sigmob| ```js var rewardedVideoAd = uni.createRewardedVideoAd(Options); var provider = rewardedVideoAd.getProvider(); ``` ### manifest 配置@manifest 注: `Sigmob`属于小型广告联盟,收益偏低。如有条件,还需开通优量汇,快手等广告渠道以便提高收益。 `Sigmob`暂不支持打包界面的勾选,如集成需进行如下的配置变动: `Sigmob`打包需要将`HBuilderX`升级到`3.2.0`以上版本。 打开 `manifest.json` 文件,点击 “源码视图”,`uni-app` 在 `app-plus->distribute->sdkConfigs` 下添加如下内容,`5+ app` 在 `plus->distribute->plugins` 下添加如下内容: ```json { "app-plus": { "distribute": { "sdkConfigs": { "ad" : { "sigmob" : {} } } } } } ``` **注意:如果已经存在ad节点,只需要在后面追加即可,如下** ```json { "app-plus": { "distribute": { "sdkConfigs": { "ad" : { "gdt" : {}, "csj" : {}, "ks" : {}, "ks-content" : {}, "sigmob" : {} } } } } } ```