主题
叙事植入 —— NovvyInsertAd
在播放器区域内铺满展示的植入广告,一集一个实例。无 load()、无 show() —— 嵌入 NovvyInsertAdView 后,宿主只需告知「当前在屏的是哪一集」,SDK 自动完成请求 → 预载 → 到点展示 → 关闭 → 请求下一支,直到本集无货(onChainFinished)。
需要 Android SDK 1.1.2 / iOS SDK 1.1.2
两端行为一致。低于该版本的 SDK 上此广告位不生效。
对接流程
每集创建一个实例 NovvyInsertAd(adUnitId, episodeNumber)
↓
在播放器之上嵌入 NovvyInsertAdView(ad, player) // 只绑定,不请求
↓
翻页回调里对新在屏的那一集 setActive(true) // 上一集自动站下,不要手动配对
↓
onAdLoaded → onAdShowed(播放器已暂停)→ onAdClosed(播放器已恢复)
↓ ↑ │
└─────────┴────── SDK 自动请求下一支 ←──────────────────┘
↓
onChainFinished // 本集结束,终止
↓
页面回收(离开可视范围 ±1 集之外)→ destroy()宿主必守五条
- 不要循环播放(
setLooping(false))—— 循环播放器永远不会播完,剧尾创意无法触发。 - 只对新在屏的那一集调
setActive(true),上一集自动站下;不要手动配对setActive(false)。 - 广告视图必须与播放器同时挂载,不要挡在播放器的就绪状态之后。
attach决定这一集什么时候向 SDK 认领槽位,if (!ready) return spinner;这类早退会把认领推迟整整一个initialize()的时长 —— 这期间上一集仍是 active 链,仍在请求、仍在驱动播放。loading 指示器应该只盖住视频层,不要包住整个Stack。 - 相邻一集的实例建议留着(±1 页),只在更远处才
destroy()。这是实例管理上的建议,不再是 SDK 保活的必要条件 —— 链条现在活到destroy()为止,widget 重建不会再销毁它;但实例本身没了就谈不上在正确的时机destroy()。给页面加AutomaticKeepAliveClientMixin(见example/lib/story_demo_page.dart)或开allowImplicitScrolling仍是最省心的做法。NovvyInsertAdView与NovvyInsertAd应当同生共死:不要单独摘掉视图而保留实例。视图没了、实例又还是 active 链时,之后加载的创意会挂进一个离屏容器 —— 不可见、无曝光,而 SDK 仍会在插入点暂停播放器。 - 右上角留空给关闭按钮,广告展示期间禁用自有的进度拖动与点击暂停。
类成员
| 成员 | 说明 |
|---|---|
NovvyInsertAd(adUnitId, episodeNumber, episodeUrl) | episodeNumber 是这个实例代表的集数,必须显式传(翻页组件会预建相邻页,读全局上下文会串集) |
setActive(bool) | 告知这一集是否在屏。true 启动/恢复本集链条。可以在 NovvyInsertAdView 建好之前调(例如播放器还在 initialize 时就翻到了这一页),平台侧会缓冲到广告对象建立后补发 |
onAdLoaded | 一支广告已就绪,等待到点(一条链会多次触发) |
onAdShowed | 广告展示(播放器已由 SDK 暂停) |
onAdClosed | 广告关闭(播放器已恢复) |
onChainFinished | 本集广告全部结束,终止;之后再 setActive(true) 不会重启 |
onAdFailedToLoad | 请求失败 |
notifySeek(fromMs, toMs) | 可选:上报宿主发起的跳转,让 SDK 精确判断插入点是否被跳过 |
notifyMediaItemChanged() | 可选:播放器换了内容而未走 setActive(false) 时上报 |
destroy() | 释放。先于其绑定的播放器销毁 |
NovvyInsertAd.viewType | 'ai.novvy.novvy_ads/insert_ad'(内部使用) |
播放器绑定
本插件不依赖任何播放器包,宿主实现 NovvyInsertPlayerBinding 桥接自有播放器:
dart
class MyPlayerBinding implements NovvyInsertPlayerBinding {
final VideoPlayerController controller;
MyPlayerBinding(this.controller);
@override
int get currentPositionMs => controller.value.position.inMilliseconds;
@override
bool get isPlaying => controller.value.isPlaying;
@override
int get durationMs => controller.value.duration.inMilliseconds;
@override
void pause() => controller.pause();
@override
void play() => controller.play();
@override
void seekTo(int positionMs) =>
controller.seekTo(Duration(milliseconds: positionMs));
}| 成员 | 类型 | 说明 |
|---|---|---|
currentPositionMs | int | 当前播放位置(毫秒) |
isPlaying | bool | 是否正在播放 |
durationMs | int | 总时长;返回 0 则剧尾创意不会触发 |
pause() / play() / seekTo(ms) | void | 广告展示期间也必须照常执行 |
pause/play/seekTo无条件执行:广告展示时宿主自有 UI 仍在广告之上,用户仍能碰到进度条,而进度条的常规手势是「按下暂停、松手播放」。那下松手若不被撤销,剧集音轨会在广告底下响起来 —— 画面被盖住,声音是唯一症状。SDK 会重新下发pause()并把位置拽回,但前提是这三个方法照做。
示例
dart
final ad = NovvyInsertAd(adUnitId: 'your_ad_unit_id', episodeNumber: 3);
ad.onAdShowed = () { /* 已展示 */ };
ad.onAdClosed = () { /* 已关闭 */ };
ad.onChainFinished = () { /* 本集广告结束 */ };
// 播放器之上铺满:
Stack(
fit: StackFit.expand,
children: [
VideoPlayer(controller),
Positioned.fill(child: NovvyInsertAdView(ad: ad, player: myBinding)),
...yourOverlayUi, // 可以压在广告之上,右上角留空
],
)
// 翻页时:
onPageChanged: (index) { if (index == myIndex) ad.setActive(true); }
// 页面回收时:
ad.destroy();完整可运行示例见
example/lib/story_demo_page.dart(含video_player绑定与纵向翻页)。
事件回调
一集内多支广告串行,因此 Loaded/Showed/Closed 会重复触发。
| 回调 | 触发时机 |
|---|---|
onAdLoaded | 一支广告已就绪,等待到点(一条链会多次触发) |
onAdFailedToLoad(error) | 请求失败,error 取值见错误类型 |
onAdShowed | 广告展示(播放器已由 SDK 暂停) |
onAdClosed | 广告关闭(播放器已恢复) |
onChainFinished | 本集广告全部结束,终止 |
回调统一在主 isolate(UI 线程) 触发。