🎬 短剧模块
版本 PanGrowth

▶️ 短剧播放页

短剧播放页提供沉浸式播放、集数管理、广告解锁与商业化能力。插件支持 NativeView 组件(推荐)与 API 控制 两种接入模式,以及 SDK 广告模式自定义广告模式 两种解锁流程。

🎯 两种广告模式对比

对比项SDK 广告模式(common)自定义广告模式(specific)
配置adMode: 'common'(默认)adMode: 'specific'
广告提供方SDK 内置 GroMore 广告开发者自行对接的广告平台
解锁流程SDK 自动管理广告播放和解锁开发者完全控制广告展示和解锁
触发事件onUnlockStart (Listener)onShowCustomAd (Listener)
需要的回调可选:自定义解锁弹窗必须:广告展示 + 奖励回调
典型场景快速接入,依赖 SDK 广告变现已有广告体系,需自主控制

注意: 两种模式的核心区别在于谁提供广告谁控制解锁流程。SDK广告模式使用GroMore广告并由SDK自动管理解锁,自定义广告模式需要开发者自行对接广告SDK并调用解锁API。


🚀 SDK 广告模式(Common)

模式一:使用 SDK 默认弹窗

最简单的接入方式,SDK 自动处理解锁弹窗和广告播放:

DramaPlayerNativeView(
  config: DramaPlayerConfig(
    dramaId: 1008,
    episode: 1,
    adMode: 'common',          // SDK 广告模式(可省略,默认值)
    freeSet: 2,                // 前 2 集免费
    unlockSet: 3,              // 每次解锁 3 集
    hideRewardDialog: false,   // 显示 SDK 默认解锁弹窗
  ),
)

流程说明

  1. 用户观看到付费集时,SDK 自动弹出默认解锁弹窗
  2. 用户点击"解锁短剧"后,SDK 自动播放 GroMore 激励广告
  3. 广告播放完成后,SDK 自动解锁并继续播放

优点:零代码接入,SDK 完全托管流程。


模式二:使用 Flutter 自定义解锁弹窗

保留 SDK 广告,但使用 Flutter 自定义弹窗样式

class SdkAdWithCustomDialog extends StatefulWidget {
  @override
  State<SdkAdWithCustomDialog> createState() => _SdkAdWithCustomDialogState();
}

class _SdkAdWithCustomDialogState extends State<SdkAdWithCustomDialog> {
  String? _playerId;
  DramaInfo? _unlockDramaInfo;
  int? _unlockEpisode;
  int? _unlockTotalEpisodes;

  void _showCustomDialog() {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => AlertDialog(
        title: Text('解锁后续剧集'),
        content: Text('观看广告后可解锁接下来的 3 集'),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              // 通知 SDK 用户取消解锁
              PangrowthContent.cancelUnlock(_playerId!);
            },
            child: Text('残忍离开'),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.pop(context);
              // 通知 SDK 用户确认解锁,SDK 将自动播放广告
              PangrowthContent.confirmUnlock(_playerId!);
            },
            child: Text('解锁短剧'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return DramaPlayerNativeView(
      config: DramaPlayerConfig(
        dramaId: 1008,
        episode: 1,
        adMode: 'common',          // SDK 广告模式
        freeSet: 2,
        unlockSet: 3,
        hideRewardDialog: true,    // ⚠️ 隐藏 SDK 默认弹窗
      ),
      listener: DramaPlayerListener(
        onPlayerReady: (playerId) {
          _playerId = playerId;
          debugPrint('播放器就绪: $playerId');
        },
        onUnlockStart: (dramaInfo, episode, totalEpisodes) {
          // 收到解锁流程开始事件,显示自定义弹窗
          setState(() {
            _unlockDramaInfo = dramaInfo;
            _unlockEpisode = episode;
            _unlockTotalEpisodes = totalEpisodes;
          });
          _showCustomDialog();
        },
        onUnlockEnd: (success) {
          debugPrint('解锁流程结束,结果: $success');
        },
      ),
    );
  }
}

流程说明

  1. 设置 hideRewardDialog: true 隐藏 SDK 默认弹窗
  2. 通过 DramaPlayerListener.onUnlockStart 监听解锁流程开始
  3. 收到事件后显示 Flutter 自定义弹窗(可获取 dramaInfo、episode、totalEpisodes 参数)
  4. 用户点击"解锁短剧"后,调用 PangrowthContent.confirmUnlock(playerId)
  5. SDK 收到确认信号后,自动播放 GroMore 激励广告
  6. 广告播放完成后,SDK 自动解锁并继续播放
  7. 通过 onUnlockEnd 回调获取解锁结果

关键 API

  • PangrowthContent.confirmUnlock(playerId) - 确认解锁,SDK 播放广告
  • PangrowthContent.cancelUnlock(playerId) - 取消解锁,继续当前播放

🎨 自定义广告模式(Specific)

完全控制广告流程,适合已有广告体系或需要特殊广告逻辑的场景:

class CustomAdMode extends StatefulWidget {
  @override
  State<CustomAdMode> createState() => _CustomAdModeState();
}

class _CustomAdModeState extends State<CustomAdMode> {
  final DramaPlayerController _controller = DramaPlayerController();
  DramaInfo? _unlockDramaInfo;

  // 阶段1: SDK 触发自定义广告事件,显示解锁确认弹窗
  void _handleCustomAdTrigger() {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => AlertDialog(
        title: Text('解锁后续剧集'),
        content: Text('观看 5 秒广告后可解锁接下来的 3 集'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('残忍离开'),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.pop(context);
              _showCustomRewardAd(); // 显示自定义广告
            },
            child: Text('解锁短剧'),
          ),
        ],
      ),
    );
  }

  // 阶段2: 显示自定义激励广告
  Future<void> _showCustomRewardAd() async {
    try {
      // 通知 SDK 广告即将展示
      await _controller.setCustomAdOnShow(cpm: 'custom_ad_cpm');

      showDialog(
        context: context,
        barrierDismissible: false,
        builder: (context) => _CustomAdWidget(
          onAdComplete: () {
            Navigator.pop(context);
            _unlockDramaAfterAd(); // 广告播放完成,执行解锁
          },
          onAdError: (error) {
            Navigator.pop(context);
            _handleAdError(error); // 广告加载失败,显示错误提示
          },
        ),
      );
    } catch (e) {
      debugPrint('❌ 通知广告展示失败: $e');
      _handleAdError('广告展示失败,请稍后重试');
    }
  }

  // 阶段3: 广告播放完成后,通知 SDK 奖励成功
  Future<void> _unlockDramaAfterAd() async {
    try {
      // 通知 SDK 广告奖励成功
      await _controller.setCustomAdOnReward(verify: true);

      // SDK 会自动解锁并继续播放
      debugPrint('✅ 短剧解锁成功!');
    } catch (e) {
      debugPrint('❌ 通知奖励失败: $e');
      // 奖励通知失败,广告已播放但解锁失败
      _handleAdError('解锁失败,请联系客服');
    }
  }

  // 错误处理
  void _handleAdError(String error) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(error)),
    );
    // 通知 SDK 广告奖励失败
    _controller.setCustomAdOnReward(verify: false);
  }

  @override
  Widget build(BuildContext context) {
    return DramaPlayerNativeView(
      config: DramaPlayerConfig(
        dramaId: 1008,
        episode: 1,
        adMode: 'specific',        // ⚠️ 自定义广告模式
        freeSet: 2,
        unlockSet: 3,
        hideRewardDialog: true,    // 隐藏 SDK 弹窗(自定义模式必须)
      ),
      controller: _controller,
      listener: DramaPlayerListener(
        onPlayerReady: (playerId) {
          debugPrint('播放器就绪: $playerId');
        },
        onShowCustomAd: (dramaInfo) {
          // 收到自定义广告触发事件
          setState(() {
            _unlockDramaInfo = dramaInfo;
          });
          _handleCustomAdTrigger();
        },
      ),
    );
  }
}

// 自定义广告组件(示例)
class _CustomAdWidget extends StatefulWidget {
  final VoidCallback onAdComplete;
  final Function(String)? onAdError;

  _CustomAdWidget({
    required this.onAdComplete,
    this.onAdError,
  });

  @override
  State<_CustomAdWidget> createState() => _CustomAdWidgetState();
}

class _CustomAdWidgetState extends State<_CustomAdWidget> {
  int _countdown = 5;

  @override
  void initState() {
    super.initState();
    _startCountdown();
  }

  void _startCountdown() {
    Future.delayed(Duration(seconds: 1), () {
      if (_countdown > 1) {
        setState(() => _countdown--);
        _startCountdown();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Dialog(
      child: Container(
        padding: EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('广告播放中...', style: TextStyle(fontSize: 18)),
            SizedBox(height: 16),
            Text('$_countdown 秒', style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold)),
            SizedBox(height: 16),
            if (_countdown <= 1)
              ElevatedButton(
                onPressed: widget.onAdComplete,
                child: Text('解锁短剧'),
              ),
          ],
        ),
      ),
    );
  }
}

流程说明

  1. 配置 adMode: 'specific' 启用自定义广告模式
  2. 通过 DramaPlayerListener.onShowCustomAd 监听 SDK 触发的自定义广告事件
  3. 收到事件后显示 Flutter 自定义解锁确认弹窗(可获取 dramaInfo 参数)
  4. 用户确认后,调用 setCustomAdOnShow() 通知 SDK 广告即将展示
  5. 显示自定义广告(对接第三方广告 SDK)
  6. 广告播放完成后,调用 setCustomAdOnReward(verify: true) 通知 SDK 奖励成功
  7. SDK 自动解锁并继续播放

关键 API

  • controller.setCustomAdOnShow(cpm: String) - 通知 SDK 广告即将展示
  • controller.setCustomAdOnReward(verify: bool) - 通知 SDK 广告奖励结果
    • verify: true - 广告播放完成,解锁短剧
    • verify: false - 广告播放失败,不解锁

🧩 DramaPlayerConfig 核心参数

基础播放

参数类型说明
dramaIdint必填,短剧 ID
episodeint起始集数,默认 1
adModeString广告模式:<br/>• 'common' - SDK 广告模式(默认)<br/>• 'specific' - 自定义广告模式
freeSetint?免费观看集数
unlockSetint?激励广告每次解锁的集数
hideRewardDialogbool?隐藏 SDK 默认解锁弹窗:<br/>• false - 使用 SDK 默认弹窗(推荐)<br/>• true - 使用 Flutter 自定义弹窗
hideBackbool?控制返回按钮显示:<br/>• falsenull - 显示 Flutter 层返回按钮(默认)<br/>• true - 完全隐藏返回按钮<br/>注意:NativeView 模式下原生返回按钮已被替换为 Flutter 层返回按钮
currentDurationint?以毫秒为单位的起播进度

推荐与埋点

参数类型说明
fromGidString?入口视频 GID,提升推荐准确度
fromSourceString?来源标识(如 drama_homedrama_card
recMapMap<String, dynamic>?自定义推荐埋点参数

UI 与交互开关

参数类型说明
hideBackbool?控制返回按钮显示(详见"返回按钮行为"章节)
hideTopInfobool?隐藏顶部信息区域
hideBottomInfobool?隐藏底部信息区域
hideMorebool?隐藏"更多"按钮
hideLikeIcon / hideCollectIconbool?隐藏点赞 / 收藏按钮
hideCellularToastbool?隐藏移动网络提示
disableDoubleClickLikebool?禁用双击点赞

布局与展示

参数类型说明
topOffset / bottomOffsetint?顶部/底部偏移量(dp)
scriptTipsTopMarginint?改编提示文案上边距
fromTopMargindouble?iOS 来源信息上边距
infiniteScrollEnabledbool?开启/关闭无限下滑(Android)
closeInfiniteScrollbool?iOS 对应语义,需与上方取反使用
presentationStyleString?iOS 展示样式:fullScreen/pageSheet/formSheet
containerIdint?Android Fragment 容器 ID(当在原生容器中展示时)

自定义广告(高级)

参数类型说明
drawAdPositionsList<int>?自定义 Draw 广告插入位置
customDrawAdViewIdString?对应的 PlatformView ID
customBannerAdViewIdString?自定义 Banner 广告视图 ID

🎮 播放控制方法

DramaPlayerController(NativeView 模式推荐)

final controller = DramaPlayerController();

// 播放控制
await controller.refresh();        // 刷新播放器

// 集数控制
await controller.setCurrentEpisode(5);       // 切换到第 5 集
final episode = await controller.getCurrentEpisode();  // 查询当前集数
await controller.openDramaGallery();         // 打开选集面板

// 播放速度
await controller.setSpeed(150);    // 1.5x 倍速(100=1.0x, 150=1.5x, 200=2.0x)

// 其他
await controller.openMoreDialog(); // 打开更多对话框

// 自定义广告专用 API
await controller.setCustomAdOnShow(cpm: 'ad_cpm');      // 通知广告展示
await controller.setCustomAdOnReward(verify: true);     // 通知广告奖励成功

PangrowthContent 静态方法(API 模式或 NativeView 均可用)

// 集数控制
await PangrowthContent.setCurrentEpisode(playerId, episode);
await PangrowthContent.getCurrentEpisode(playerId);
await PangrowthContent.openEpisodePanel(playerId);

// SDK 广告模式 + 自定义弹窗专用
await PangrowthContent.confirmUnlock(playerId);  // 确认解锁(SDK 播放广告)
await PangrowthContent.cancelUnlock(playerId);   // 取消解锁

// 生命周期管理
await PangrowthContent.destroyDramaPlayer(playerId);

📡 事件监听

DramaPlayerListener(组件级回调,推荐)

适合在 DramaPlayerNativeView 中直接使用,更新 Flutter UI 状态:

DramaPlayerNativeView(
  config: config,
  listener: DramaPlayerListener(
    // 生命周期回调
    onPlayerReady: (playerId) {
      debugPrint('播放器就绪: $playerId');
    },
    onPlayerDisposed: () {
      debugPrint('播放器已销毁');
    },
    onPlayerError: (error) {
      debugPrint('播放器错误: $error');
    },

    // 播放状态回调
    onEpisodeChange: (episode) {
      setState(() => _currentEpisode = episode);
    },

    // 解锁流程回调(SDK 广告模式)
    onUnlockStart: (dramaInfo, episode, totalEpisodes) {
      debugPrint('解锁流程开始: 第$episode集 / 共$totalEpisodes集');
      debugPrint('短剧标题: ${dramaInfo?.title}');
      // 显示自定义解锁弹窗...
    },
    onUnlockEnd: (success) {
      debugPrint('解锁流程结束,结果: $success');
    },

    // 自定义广告回调(自定义广告模式)
    onShowCustomAd: (dramaInfo) {
      debugPrint('SDK 触发自定义广告事件');
      debugPrint('短剧标题: ${dramaInfo?.title}');
      // 显示自定义广告...
    },
  ),
)

可用的回调方法

生命周期回调

  • onPlayerReady(String playerId) - 播放器创建完成
  • onPlayerDisposed() - 播放器销毁
  • onPlayerError(String error) - 播放器错误
  • onPlayerClose() - 播放器关闭

播放控制回调

  • onVideoStart(DramaInfo? dramaInfo) - 视频开始播放
  • onVideoOverPlay() - 视频播放结束
  • onVideoPause() - 视频暂停
  • onVideoContinue() - 视频恢复播放
  • onVideoCompletion() - 整个短剧播放完成

集数与短剧切换回调

  • onEpisodeChange(int episode) - 集数切换
  • onDramaSwitch(DramaInfo drama) - 短剧切换

解锁流程回调

  • onUnlockStart(DramaInfo? dramaInfo, int? episode, int? totalEpisodes) - 解锁流程开始(SDK 广告模式)
  • onUnlockEnd(bool success) - 解锁流程结束
  • onShowCustomAd(DramaInfo? dramaInfo) - 显示自定义广告(自定义广告模式)

广告事件回调

  • onAdRequest() - 广告请求
  • onAdLoadSuccess() - 广告加载成功
  • onAdLoadFail(int errorCode, String errorMessage) - 广告加载失败
  • onAdShow() - 广告展示
  • onAdClick() - 广告点击
  • onAdRewardFinish() - 激励广告奖励完成

OnContentEventListener(全局事件流)

适合埋点、监听自定义广告事件、解锁事件等:

class MyEventListener implements OnContentEventListener {
  @override
  void onDramaPlayerEvent(DramaPlayerEvent event) {
    switch (event.action) {
      case 'drama_unlock_start':      // SDK 广告模式 - 解锁流程开始
        debugPrint('解锁流程开始,显示自定义弹窗');
        break;
      case 'drama_show_custom_ad':    // 自定义广告模式 - SDK 触发广告
        debugPrint('SDK 触发自定义广告事件');
        break;
      case 'drama_unlock_end':        // 解锁流程结束
        final success = event.data['success'] as bool;
        debugPrint('解锁结束,结果: $success');
        break;
      case 'drama_video_start_play':  // 视频开始播放
      case 'drama_video_over_play':   // 视频播放结束
      case 'drama_episode_change':    // 集数切换
        // 埋点统计...
        break;
    }
  }

  // 其他接口留空或按需实现
  @override
  void onContentEvent(ContentEvent event) {}
  @override
  void onPlayEvent(PlayEvent event) {}
  // ...
}

// 注册监听
await PangrowthContent.onEventListener(MyEventListener());

常见事件 action

  • drama_player_created - 播放器创建成功
  • drama_player_shown - 播放器展示完成
  • drama_unlock_start - SDK 广告模式:解锁流程开始
  • drama_show_custom_ad - 自定义广告模式:SDK 触发广告
  • drama_unlock_end - 解锁流程结束
  • drama_video_start_play - 视频开始播放
  • drama_video_over_play - 视频播放结束
  • drama_episode_change - 集数切换
  • drama_player_destroyed - 播放器销毁

⚠️ 返回按钮行为说明

NativeView 模式的返回按钮处理

DramaPlayerNativeView 模式下,原生 SDK 的返回按钮已被自动替换为 Flutter 层返回按钮,以解决原生返回按钮的兼容性问题。

行为特点

  • 自动替换:插件内部强制隐藏原生返回按钮,使用 Flutter 层按钮替代
  • 样式统一:Flutter 返回按钮样式与原生保持一致(圆形半透明背景 + 返回图标)
  • 位置固定:左上角 SafeArea 内,距离边缘 16dp
  • 默认行为:点击调用 Navigator.pop(context) 返回上一页

控制返回按钮显示

// 显示返回按钮(默认行为)
DramaPlayerNativeView(
  config: DramaPlayerConfig(
    dramaId: 1008,
    hideBack: false,  // 或省略此参数
  ),
)

// 完全隐藏返回按钮
DramaPlayerNativeView(
  config: DramaPlayerConfig(
    dramaId: 1008,
    hideBack: true,  // 不显示返回按钮
  ),
)

自定义返回逻辑

如需自定义返回行为,建议使用 WillPopScope 或 Flutter 3.x 的 PopScope

PopScope(
  canPop: false,  // 禁用系统返回
  onPopInvoked: (didPop) {
    if (!didPop) {
      // 自定义返回逻辑
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: const Text('确认退出'),
          content: const Text('确定要退出播放吗?'),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('取消'),
            ),
            TextButton(
              onPressed: () {
                Navigator.pop(context);  // 关闭对话框
                Navigator.pop(context);  // 退出播放页
              },
              child: const Text('确定'),
            ),
          ],
        ),
      );
    }
  },
  child: DramaPlayerNativeView(
    config: DramaPlayerConfig(
      dramaId: 1008,
      hideBack: true,  // 隐藏默认返回按钮,使用系统返回键
    ),
  ),
)

API 模式的返回按钮

API 模式下,返回按钮行为由原生 SDK 控制,hideBack 参数直接传递给原生端。


🚪 接入方式概览

方式推荐场景实现特点
NativeView 组件(推荐)Flutter 页面直接内嵌播放页使用 DramaPlayerNativeView,配合 DramaPlayerControllerDramaPlayerListener,生命周期自动托管,返回按钮已替换为 Flutter 层实现
API 调用需要在原生容器打开或沿用既有流程createDramaPlayer / showDramaPlayer / destroyDramaPlayer 主动控制,适合弹窗、Fragment 等场景

NativeView 组件模式(推荐)

import 'package:flutter/material.dart';
import 'package:pangrowth_content/pangrowth_content.dart';

class DramaPlayerScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: DramaPlayerNativeView(
        config: DramaPlayerConfig(
          dramaId: 1008,
          episode: 1,
          freeSet: 2,
          unlockSet: 3,
        ),
      ),
    );
  }
}

优点

  • ✅ 生命周期自动管理(dispose 时自动销毁播放器)
  • ✅ 结合 DramaPlayerControllerDramaPlayerListener 更便捷
  • ✅ 支持 Flutter 层级嵌套和布局

API 调用模式

适合在 Flutter 之外的容器打开播放页,或兼容旧有流程:

class DramaPlayerLauncher {
  String? _playerId;

  Future<void> openPlayer() async {
    final result = await PangrowthContent.createDramaPlayer(
      DramaPlayerConfig(
        dramaId: 1008,
        episode: 1,
        adMode: 'common',
      ),
    );

    if (result['success'] != true) {
      debugPrint('创建播放页失败: $result');
      return;
    }

    _playerId = result['playerId'] as String;

    await PangrowthContent.showDramaPlayer(
      _playerId!,
      presentationStyle: 'fullScreen',  // iOS 展示样式
    );
  }

  Future<void> closePlayer() async {
    if (_playerId == null) return;
    await PangrowthContent.destroyDramaPlayer(_playerId!);
    _playerId = null;
  }
}

showDramaPlayer 还支持 containerId(Android Fragment 容器)和 presentationStyle(iOS 弹窗样式)。


🌍 平台差异说明

  • presentationStyle / fromTopMargin / playStartTime 等仅在 iOS 生效;Android 会忽略。
  • containerIdinfiniteScrollEnabled 等主要影响 Android 原生容器。
  • NativeView 模式下,iOS 通过 UiKitView 承载,Android 通过 AndroidViewSurface;如需覆盖蒙层,请使用透明背景的整屏 Stack 并避免直接嵌套在播放器之上。

💡 最佳实践

1. 选择合适的广告模式

  • 快速接入,依赖 SDK 广告变现 → 使用 SDK 广告模式(common) + SDK 默认弹窗
  • 自定义弹窗样式,但使用 SDK 广告 → 使用 SDK 广告模式(common) + hideRewardDialog: true + confirmUnlock API
  • 已有第三方广告体系 → 使用 自定义广告模式(specific) + 自行对接广告 SDK

2. 事件监听最佳实践

使用 DramaPlayerListener 处理组件级状态更新和业务逻辑(如集数切换、播放器就绪、广告触发、解锁流程等)。

3. 生命周期管理

  • NativeView 模式:销毁时自动释放播放器,无需手动调用 destroyDramaPlayer
  • API 模式:必须手动调用 destroyDramaPlayer 避免内存泄漏

4. 错误处理

DramaPlayerListener(
  onPlayerError: (error) {
    debugPrint('播放器错误: $error');
    // 根据错误类型进行处理
    if (error.contains('SDK_NOT_READY')) {
      // 提示用户 SDK 未初始化
    }
  },
)

📚 相关文档


💡 提示: 短剧播放功能需要有效的内容源和网络连接。建议在正式使用前先进行测试,确保内容加载和广告解锁正常。

需要进一步协助?

与 LightCore 技术顾问沟通,获取商业化策略与集成支持。