Skip to content

叙事植入 —— NovvyInsertAd ​

单集之内的自驱动串行广告链(story_placement,custom.format = 7)。素材首帧是为短剧的某一 瞬间专门制作的,与该帧画面贴合,所以交接读起来是「剧里插了一段」而不是「跳走看广告」。广告在 宿主自己的播放器容器内部展示——没有 Activity 转场、没有全屏切换。

与其他六位最大的不同:没有 load()。attach() 只绑定播放器、不发请求;链从 setActive(true) 起步,所以为离屏剧集预建的实例是零成本的。

本位全部策略都在原生 SDK 里

谁是 active、谁进 standby、插入点是否过期、链何时终止——JS 侧一概不推导。宿主只需要做两件事:把播放器状态喂进来,以及执行 SDK 发回来的播放器命令。

对接流程 ​

每集一个 NovvyInsertAd 实例(相邻集也建,成本为零)
        ↓
挂载 <NovvyInsertAdView>(与播放器同时挂,不要等 ready)
        ↓  视图就绪 → 自动 attach(binding):绑定播放器,不发请求
        ↓
这一集滑到屏上 → setActive(true)      // 唯一的策略信号
        ↓
onAdLoaded                             // 一支素材预载完成(链里每支都回调一次)
        ↓
到达插入点 → SDK 暂停播放器、揭示广告
        ↓
onAdShowed                             // 广告可见,曝光在此之后按可见性判据上报
        ↓
用户关闭(或自动关闭)→ SDK 恢复播放
        ↓
onAdClosed                             // 链继续,等下一个插入点
        ↓
onChainFinished                        // 终态:本集不会再有广告
        ↓
离开可视范围 ±1 集之外 → destroy()      // 唯一真销毁路径

构造函数:new NovvyInsertAd(adUnitId: string, episodeNumber?: number | null, episodeUrl?: string | null)

episodeNumber 显式传入而不是读 setContentContext:竖滑 pager 会预建相邻页,读全局值会把请求 标记成「当前正在播的那一集」。

类成员 ​

成员说明
attach(binding: NovvyInsertPlayerBinding)绑定播放器,不发请求(通常由 <NovvyInsertAdView> 自动调用)。原生侧幂等:视图重建后再次 attach 会接管既有链,不会开第二条
setActive(active: boolean)告诉 SDK 这一集是否在屏上。只在新在屏那一集调 true,永不配对 false(见下方必守五条第 2 条)。可在视图存在之前调用,平台侧会缓冲到广告对象建好
notifySeek(fromMs, toMs)上报一次精确 seek。可选但建议接:没有它,SDK 只能用「位置增量 vs 墙钟」的启发式判断,且该启发式偏保守——宁可丢掉一支广告,也不把它展示在错误的帧上
notifyMediaItemChanged()播放器被指向了别的内容(播放列表式换集)却没走 setActive(false) 时上报
detach()只解绑视图,不结束链(通常由组件卸载时自动调用)。链继续持有它在 SDK 注册表里的槽位与已预载的素材
destroy()唯一真销毁路径。释放链与原生实例
onAdLoaded() => void,一支素材预载完成。链里每支都会回调,不是一次性的
onAdFailedToLoad(error)(error: string) => void,一次加载失败。不会自动 destroy——SDK 会自行终止链并补发 onChainFinished
onAdShowed() => void,广告揭示(播放器此刻已被 SDK 暂停)
onAdClosed() => void,一支广告关闭(播放器已恢复)。链还在继续,这不是终态
onChainFinished() => void,终态:本集不会再请求广告,之后再 setActive(true) 也不会重启
NovvyInsertAd.viewType'NovvyInsertAdView'(静态常量)

播放器接缝 NovvyInsertPlayerBinding ​

Pull 模型:SDK 需要时读三个 getter,宿主只要保证它们能随时答出最新值即可,不必主动推。

成员说明
currentPositionMsnumber(getter),当前播放位置(毫秒)
isPlayingboolean(getter),是否正在播放(非暂停/缓冲)
durationMsnumber(getter),总时长。返回 0 则剧尾创意永远不会触发
pause()暂停宿主播放
play()恢复宿主播放
seekTo(positionMs)把宿主播放位置移到 positionMs

⚠️ pause / play / seekTo 在广告展示期间也必须照常执行。 宿主自有 UI 仍浮在广告之上,进度条 的常规手势是「按下暂停、松手播放」;那下松手若不被撤销,剧集音轨会在广告底下响——画面被广告盖 住了,声音是唯一症状,所以这条最容易破、也最难自查。

宿主必守五条 ​

  1. 不要循环播放(repeat={false})。循环播放器永远不会播完,剧尾创意无法触发。
  2. 只对新在屏那一集调 setActive(true),上一集自动站下,不要手动配对 setActive(false)。 认领动作本身就会让上一集站下,而这也正是「上一集的广告被保活、滑回去还能接着看」的来源;手动 配对 false 会把 standby 槽位打掉。
  3. 广告视图必须与播放器同时挂载,不要挡在 ready 之后。 attach 决定这一集何时向 SDK 认领槽位, if (!ready) return <Spinner/> 会把认领整整推迟一个视频 initialize() 的时长,这期间上一集仍是 active 链。
  4. NovvyInsertAdView 与 NovvyInsertAd 同生共死,且相邻一集(±1 页)建议留着。 视图没了而实例还是 active 链时,之后加载的创意会挂进离屏容器——不可见、无曝光,而 SDK 仍会在 插入点暂停播放器。
  5. 右上角留空给关闭药丸,广告展示期间禁用自有的拖动与点击暂停。

完整示例(竖滑 + react-native-video) ​

tsx
function EpisodePage({ episode, isActive }: { episode: Episode; isActive: boolean }) {
  const videoRef = useRef<VideoRef>(null);
  // Pull 模型:getter 要答最新值,所以用 ref 而不是 state
  const positionMs = useRef(0);
  const durationMs = useRef(0);
  const playing = useRef(false);

  const binding = useMemo<NovvyInsertPlayerBinding>(() => ({
    get currentPositionMs() { return positionMs.current; },
    get isPlaying() { return playing.current; },
    get durationMs() { return durationMs.current; },
    pause: () => videoRef.current?.pause(),      // 展示期间也要照常执行
    play: () => videoRef.current?.resume(),
    seekTo: (ms) => videoRef.current?.seek(ms / 1000),
  }), []);

  const ad = useMemo(() => new NovvyInsertAd(AD_UNIT, episode.number), [episode.number]);

  useEffect(() => {
    ad.onAdShowed = () => {};
    ad.onAdClosed = () => {};
    ad.onChainFinished = () => {};
    return () => ad.destroy();                    // 第 4 条:页面卸载才 destroy
  }, [ad]);

  useEffect(() => {
    if (isActive) ad.setActive(true);             // 第 2 条:只 claim,不配对 false
  }, [ad, isActive]);

  return (
    <View style={{ height: SCREEN_HEIGHT }}>
      <Video
        ref={videoRef}
        source={{ uri: episode.uri }}
        style={StyleSheet.absoluteFill}
        repeat={false}                            // 第 1 条
        paused={!isActive}
        onLoad={({ duration }) => { durationMs.current = Math.round(duration * 1000); }}
        onProgress={({ currentTime }) => { positionMs.current = Math.round(currentTime * 1000); }}
        onPlaybackStateChanged={({ isPlaying }) => { playing.current = isPlaying; }}
        onSeek={({ currentTime, seekTime }) =>
          ad.notifySeek(Math.round(currentTime * 1000), Math.round(seekTime * 1000))
        }
      />
      {/* 第 3 条:无条件挂载,不要等播放器 ready */}
      <NovvyInsertAdView ad={ad} binding={binding} />
    </View>
  );
}

完整可运行版本见 example/StoryDemoScreen.tsx。

事件回调 ​

与其他类型都不同,因为它是一条链而不是一支广告:

回调触发时机
onAdLoaded一支素材预载完成。链里每支都回调一次,不是一次性的「就绪」转换
onAdFailedToLoad(error)一次加载失败。不会自动 destroy —— SDK 自行终止链并补发 onChainFinished;error 取值见错误类型
onAdShowed广告揭示(不代表曝光已上报,见回调与曝光)
onAdDismissed / onAdClosed链里的一支广告关闭;链继续,这不是终态
onChainFinished终态:本集不会再请求广告,之后再 setActive(true) 也不重启

切集不产生回调。 滑走不会回调 onAdClosed,滑回来也不会再回调 onAdShowed —— 恢复的是同一支广告,曝光只上报一次。上一集只是被 SDK 挪进 standby 槽位,不是关闭。

回调统一在 JS 主线程 触发。

本文档仅供技术对接使用,请勿外传。