|
|
|
|
import { MiniGamePlatform } from '../platform/MiniGamePlatform';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 激励视频广告(微信 / 抖音 / 快手 / B 站 / OPPO / QQ)。
|
|
|
|
|
*
|
|
|
|
|
* 官方要点:
|
|
|
|
|
* - 先 onLoad/onError/onClose,再 load/show(抖音)
|
|
|
|
|
* - 微信/快手:全局单例,show 失败时 load→show(微信文档)
|
|
|
|
|
* - B 站:show 前 reportScene(1007)
|
|
|
|
|
* - onClose 仅以 isEnded 判定奖励(快手);微信兼容 res===undefined
|
|
|
|
|
*/
|
|
|
|
|
export class RewardedVideoAd {
|
|
|
|
|
|
|
|
|
|
static keys: string[] = [];
|
|
|
|
|
private static _keyIndex = 0;
|
|
|
|
|
private static _errCount = 0;
|
|
|
|
|
private static _callback: Function | null = null;
|
|
|
|
|
/** 非单例平台当前实例 */
|
|
|
|
|
private static _ad: any = null;
|
|
|
|
|
private static _currentUnitId = '';
|
|
|
|
|
/** 微信/快手:adUnitId → 单例实例 */
|
|
|
|
|
private static _singletonAds = new Map<string, any>();
|
|
|
|
|
private static _eventsBound = new WeakSet<object>();
|
|
|
|
|
/** 防止 onClose 重复触发导致业务回调执行两次 */
|
|
|
|
|
private static _sessionFinished = false;
|
|
|
|
|
|
|
|
|
|
static init(keys: string[]) {
|
|
|
|
|
this.keys = keys ?? [];
|
|
|
|
|
MiniGamePlatform.init();
|
|
|
|
|
if (this.keys.length > 0 && MiniGamePlatform.pf?.createRewardedVideoAd) {
|
|
|
|
|
const ad = this._ensureAd(this.keys[0]);
|
|
|
|
|
ad?.load?.();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static show(callback?: Function) {
|
|
|
|
|
this._sessionFinished = false;
|
|
|
|
|
this._callback = callback ?? null;
|
|
|
|
|
this._errCount = 0;
|
|
|
|
|
const pf = MiniGamePlatform.pf;
|
|
|
|
|
if (!pf?.createRewardedVideoAd) {
|
|
|
|
|
this._logFail('AD:REWARD_NO_API');
|
|
|
|
|
this._finish(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const adUnitId = this.keys[this._keyIndex];
|
|
|
|
|
if (!adUnitId) {
|
|
|
|
|
this._logFail('AD:REWARD_NO_UNIT');
|
|
|
|
|
this._finish(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const ad = this._ensureAd(adUnitId);
|
|
|
|
|
if (!ad) {
|
|
|
|
|
this._logFail('AD:REWARD_CREATE_FAIL', adUnitId);
|
|
|
|
|
this._finish(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._ad = ad;
|
|
|
|
|
MiniGamePlatform.reportRewardedVideoScene();
|
|
|
|
|
this._showWithReload(ad);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 激励视频会话是否仍在等待 onClose(用于广告挂起态判断) */
|
|
|
|
|
static hasPendingCallback(): boolean {
|
|
|
|
|
return this._callback != null && !this._sessionFinished;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _ensureAd(adUnitId: string): any {
|
|
|
|
|
const pf = MiniGamePlatform.pf;
|
|
|
|
|
if (!pf?.createRewardedVideoAd) return null;
|
|
|
|
|
|
|
|
|
|
if (MiniGamePlatform.isRewardedVideoAdSingleton()) {
|
|
|
|
|
let ad = this._singletonAds.get(adUnitId);
|
|
|
|
|
if (ad) return ad;
|
|
|
|
|
ad = pf.createRewardedVideoAd({ adUnitId });
|
|
|
|
|
if (!ad) return null;
|
|
|
|
|
this._bindEvents(ad);
|
|
|
|
|
this._singletonAds.set(adUnitId, ad);
|
|
|
|
|
return ad;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this._ad && this._currentUnitId === adUnitId) {
|
|
|
|
|
return this._ad;
|
|
|
|
|
}
|
|
|
|
|
if (this._ad) {
|
|
|
|
|
this._destroyAd(this._ad);
|
|
|
|
|
this._ad = null;
|
|
|
|
|
}
|
|
|
|
|
const ad = pf.createRewardedVideoAd({ adUnitId });
|
|
|
|
|
if (!ad) return null;
|
|
|
|
|
this._currentUnitId = adUnitId;
|
|
|
|
|
this._bindEvents(ad);
|
|
|
|
|
ad.load?.();
|
|
|
|
|
return ad;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 官方推荐:先注册监听,再 load/show */
|
|
|
|
|
private static _bindEvents(ad: any): void {
|
|
|
|
|
if (this._eventsBound.has(ad)) return;
|
|
|
|
|
this._eventsBound.add(ad);
|
|
|
|
|
ad.onLoad?.(() => {
|
|
|
|
|
console.log('[RewardedVideoAd] onLoad');
|
|
|
|
|
});
|
|
|
|
|
ad.onError?.((err: unknown) => {
|
|
|
|
|
console.log('[RewardedVideoAd] onError', err);
|
|
|
|
|
this._logFail('AD:REWARD_ON_ERROR', err);
|
|
|
|
|
this._onLoadError();
|
|
|
|
|
});
|
|
|
|
|
ad.onClose?.((res?: { isEnded?: boolean }) => {
|
|
|
|
|
const ok = MiniGamePlatform.isRewardedVideoCompleted(res);
|
|
|
|
|
console.log('[RewardedVideoAd] onClose', res, 'completed=', ok);
|
|
|
|
|
this._finish(ok);
|
|
|
|
|
ad.load?.();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** show 失败时 load→show(微信/快手/B站官方示例) */
|
|
|
|
|
private static _showWithReload(ad: any): void {
|
|
|
|
|
let showResult: { then?: Function; catch?: Function } | undefined;
|
|
|
|
|
try {
|
|
|
|
|
showResult = ad.show?.();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.log('[RewardedVideoAd] show throw', err);
|
|
|
|
|
this._logFail('AD:REWARD_SHOW_THROW', err);
|
|
|
|
|
this._onLoadError();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!showResult?.then) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
showResult.catch((err: unknown) => {
|
|
|
|
|
console.log('[RewardedVideoAd] show fail, load then show', err);
|
|
|
|
|
this._logFail('AD:REWARD_SHOW_FAIL', err);
|
|
|
|
|
const loadResult = ad.load?.();
|
|
|
|
|
if (!loadResult?.then) {
|
|
|
|
|
ad.load?.();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
loadResult
|
|
|
|
|
.then(() => {
|
|
|
|
|
const retry = ad.show?.();
|
|
|
|
|
if (retry?.then) {
|
|
|
|
|
retry.catch((err2: unknown) => {
|
|
|
|
|
console.log('[RewardedVideoAd] load+show fail', err2);
|
|
|
|
|
this._logFail('AD:REWARD_LOAD_SHOW_FAIL', err2);
|
|
|
|
|
this._onLoadError();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((err2: unknown) => {
|
|
|
|
|
console.log('[RewardedVideoAd] load+show fail', err2);
|
|
|
|
|
this._logFail('AD:REWARD_LOAD_FAIL', err2);
|
|
|
|
|
this._onLoadError();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _onLoadError(): void {
|
|
|
|
|
this._errCount++;
|
|
|
|
|
if (this._errCount >= 5 || this.keys.length === 0) {
|
|
|
|
|
this._finish(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const oldKey = this.keys[this._keyIndex];
|
|
|
|
|
this._keyIndex = (this._keyIndex + 1) % this.keys.length;
|
|
|
|
|
const newKey = this.keys[this._keyIndex];
|
|
|
|
|
if (MiniGamePlatform.isRewardedVideoAdSingleton()) {
|
|
|
|
|
this._singletonAds.delete(oldKey);
|
|
|
|
|
} else if (this._ad) {
|
|
|
|
|
this._destroyAd(this._ad);
|
|
|
|
|
this._ad = null;
|
|
|
|
|
this._currentUnitId = '';
|
|
|
|
|
}
|
|
|
|
|
const ad = this._ensureAd(newKey);
|
|
|
|
|
if (!ad) {
|
|
|
|
|
this._logFail('AD:REWARD_SWITCH_FAIL', newKey);
|
|
|
|
|
this._finish(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._ad = ad;
|
|
|
|
|
this._showWithReload(ad);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _destroyAd(ad: any): void {
|
|
|
|
|
if (!ad?.destroy || MiniGamePlatform.isRewardedVideoAdSingleton()) return;
|
|
|
|
|
try {
|
|
|
|
|
ad.destroy();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[RewardedVideoAd] destroy failed', e);
|
|
|
|
|
}
|
|
|
|
|
this._eventsBound.delete(ad);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _finish(success: boolean): void {
|
|
|
|
|
if (this._sessionFinished) return;
|
|
|
|
|
this._sessionFinished = true;
|
|
|
|
|
if (!success) {
|
|
|
|
|
this._logFail('AD:REWARD_FINISH_FAIL');
|
|
|
|
|
}
|
|
|
|
|
const cb = this._callback;
|
|
|
|
|
this._callback = null;
|
|
|
|
|
if (cb) cb(success);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static _logFail(tag: string, detail?: unknown): void {
|
|
|
|
|
try {
|
|
|
|
|
gg.sdk?.pfLogKey?.(tag, detail);
|
|
|
|
|
} catch { /* ignore */ }
|
|
|
|
|
}
|
|
|
|
|
}
|