▶️ 短剧播放页
短剧播放页提供沉浸式播放、集数管理、广告解锁与商业化能力。插件支持 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 默认解锁弹窗
),
)
流程说明:
- 用户观看到付费集时,SDK 自动弹出默认解锁弹窗
- 用户点击"解锁短剧"后,SDK 自动播放 GroMore 激励广告
- 广告播放完成后,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');
},
),
);
}
}
流程说明:
- 设置
hideRewardDialog: true
隐藏 SDK 默认弹窗 - 通过
DramaPlayerListener.onUnlockStart
监听解锁流程开始 - 收到事件后显示 Flutter 自定义弹窗(可获取 dramaInfo、episode、totalEpisodes 参数)
- 用户点击"解锁短剧"后,调用
PangrowthContent.confirmUnlock(playerId)
- SDK 收到确认信号后,自动播放 GroMore 激励广告
- 广告播放完成后,SDK 自动解锁并继续播放
- 通过
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('解锁短剧'),
),
],
),
),
);
}
}
流程说明:
- 配置
adMode: 'specific'
启用自定义广告模式 - 通过
DramaPlayerListener.onShowCustomAd
监听 SDK 触发的自定义广告事件 - 收到事件后显示 Flutter 自定义解锁确认弹窗(可获取 dramaInfo 参数)
- 用户确认后,调用
setCustomAdOnShow()
通知 SDK 广告即将展示 - 显示自定义广告(对接第三方广告 SDK)
- 广告播放完成后,调用
setCustomAdOnReward(verify: true)
通知 SDK 奖励成功 - SDK 自动解锁并继续播放
关键 API:
controller.setCustomAdOnShow(cpm: String)
- 通知 SDK 广告即将展示controller.setCustomAdOnReward(verify: bool)
- 通知 SDK 广告奖励结果verify: true
- 广告播放完成,解锁短剧verify: false
- 广告播放失败,不解锁
🧩 DramaPlayerConfig
核心参数
基础播放
参数 | 类型 | 说明 |
---|---|---|
dramaId | int | 必填,短剧 ID |
episode | int | 起始集数,默认 1 |
adMode | String | 广告模式:<br/>• 'common' - SDK 广告模式(默认)<br/>• 'specific' - 自定义广告模式 |
freeSet | int? | 免费观看集数 |
unlockSet | int? | 激励广告每次解锁的集数 |
hideRewardDialog | bool? | 隐藏 SDK 默认解锁弹窗:<br/>• false - 使用 SDK 默认弹窗(推荐)<br/>• true - 使用 Flutter 自定义弹窗 |
hideBack | bool? | 控制返回按钮显示:<br/>• false 或 null - 显示 Flutter 层返回按钮(默认)<br/>• true - 完全隐藏返回按钮<br/>注意:NativeView 模式下原生返回按钮已被替换为 Flutter 层返回按钮 |
currentDuration | int? | 以毫秒为单位的起播进度 |
推荐与埋点
参数 | 类型 | 说明 |
---|---|---|
fromGid | String? | 入口视频 GID,提升推荐准确度 |
fromSource | String? | 来源标识(如 drama_home 、drama_card ) |
recMap | Map<String, dynamic>? | 自定义推荐埋点参数 |
UI 与交互开关
参数 | 类型 | 说明 |
---|---|---|
hideBack | bool? | 控制返回按钮显示(详见"返回按钮行为"章节) |
hideTopInfo | bool? | 隐藏顶部信息区域 |
hideBottomInfo | bool? | 隐藏底部信息区域 |
hideMore | bool? | 隐藏"更多"按钮 |
hideLikeIcon / hideCollectIcon | bool? | 隐藏点赞 / 收藏按钮 |
hideCellularToast | bool? | 隐藏移动网络提示 |
disableDoubleClickLike | bool? | 禁用双击点赞 |
布局与展示
参数 | 类型 | 说明 |
---|---|---|
topOffset / bottomOffset | int? | 顶部/底部偏移量(dp) |
scriptTipsTopMargin | int? | 改编提示文案上边距 |
fromTopMargin | double? | iOS 来源信息上边距 |
infiniteScrollEnabled | bool? | 开启/关闭无限下滑(Android) |
closeInfiniteScroll | bool? | iOS 对应语义,需与上方取反使用 |
presentationStyle | String? | iOS 展示样式:fullScreen /pageSheet /formSheet |
containerId | int? | Android Fragment 容器 ID(当在原生容器中展示时) |
自定义广告(高级)
参数 | 类型 | 说明 |
---|---|---|
drawAdPositions | List<int>? | 自定义 Draw 广告插入位置 |
customDrawAdViewId | String? | 对应的 PlatformView ID |
customBannerAdViewId | String? | 自定义 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 ,配合 DramaPlayerController 、DramaPlayerListener ,生命周期自动托管,返回按钮已替换为 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 时自动销毁播放器)
- ✅ 结合
DramaPlayerController
和DramaPlayerListener
更便捷 - ✅ 支持 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 会忽略。containerId
、infiniteScrollEnabled
等主要影响 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 未初始化
}
},
)
📚 相关文档
- 短剧聚合页 - 短剧列表展示和推荐
- 短剧滑滑流 - 短剧滑动播放体验
- 短剧卡片 - 短剧卡片组件
- Widget组件总览 - 所有组件导航
💡 提示: 短剧播放功能需要有效的内容源和网络连接。建议在正式使用前先进行测试,确保内容加载和广告解锁正常。