interactive_media_ads 接入实战:在 Flutter 中集成 IMA SDK 播放 VAST 视频广告
【免费下载链接】packagesA collection of useful packages maintained by the Flutter team项目地址: https://gitcode.com/GitHub_Trending/pac/packages
interactive_media_ads是 Flutter 官方插件仓库中由 Flutter 团队维护的广告插件,它将 Google Interactive Media Ads(IMA)SDK 的能力封装为 Dart API,让 Flutter 应用可以轻松地向任意遵循 VAST 规范 为主线,结合仓库中的 示例代码、核心 API 源码 与平台配置,完整讲解从 Android 工程配置、五大核心组件,到广告请求、事件处理、资源释放的端到端接入流程。读完本文,你将能够在自己的 Flutter 应用中实现"内容视频 + 前置/中插/后置广告"的完整播放体验。
IMA client-side 模式的核心思想
IMA 插件采用的是client-side(客户端)集成模式:广告请求由 App 直接向广告服务器发出,App 保留对内容视频播放的完全控制权,而 SDK 只负责广告的请求与播放。广告播放时,SDK 会使用一个独立于内容播放器的广告播放器,将其定位在内容视频之上;广告结束后,再交还控制权给内容播放器。
这意味着接入 IMA 不会改变你现有的内容播放方案——你依然使用 video_player 之类的插件播放正片,IMA 插件则像"叠加层"一样处理广告。README 中的一句话概括了这一分工:"With IMA client-side SDKs, you maintain control of content video playback, while the SDK handles ad playback."
平台支持情况
| 平台 | 支持版本 |
|---|---|
| Android | SDK 24+ |
| iOS | 13.0+ |
注意(README 原文声明):Background Audio ads(后台音频广告)与 Google Dynamic Ad Insertion(Google 动态广告插入)方法目前不受支持。
五大核心组件:IMA client-side 的骨架
README 指出,实现 IMA client-side 涉及五个主要 SDK 组件,它们也是本插件 Dart API 的核心类:
| 组件 | Dart 类 | 职责 |
|---|---|---|
| AdDisplayContainer | ad_display_container.dart | 广告渲染的容器 Widget,负责承载广告视图并处理广告点击 |
| AdsLoader | ads_loader.dart | 请求广告并处理广告请求响应的各类事件;同一时间只能实例化一个 AdsLoader,可在页面生命周期内复用 |
| AdsRequest | ads_request.dart | 定义一次广告请求:指定 VAST 广告标签(ad tag)URL、广告尺寸等附加参数 |
| AdsManager | ads_loader.dart(同文件) | 持有广告请求的响应结果,控制广告播放,并监听 SDK 抛出的广告事件 |
| AdsManagerDelegate | ads_manager_delegate.dart | 处理广告/流初始化与播放过程中发生的广告事件和错误 |
从源码结构看,这些类均采用"平台接口 + 平台实现"的分层设计(Platform*系列类在 platform_interface 下,Android/iOS 各有独立实现目录 android 与 ios),并且插件通过 Pigeon 生成平台通道代码(见 pigeons 目录),因此你可以在 Dart 层统一调用,而不必关心原生差异。
第一步:Android 工程配置(仅 Android 需要)
如果目标平台包含 Android,需要完成两项配置;若只构建 iOS 可跳过本节。
1. 更新 AndroidManifest 权限
在android/app/src/main/AndroidManifest.xml中为 IMA SDK 添加请求广告所需的用户权限(与仓库示例 AndroidManifest.xml 一致):
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <!-- Required permissions for the IMA SDK --> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> </manifest>2. 更新 App 级 Gradle 配置(启用库脱糖)
IMA SDK 要求启用library desugaring(库脱糖)。需要在android/app/build.gradle.kts中设置coreLibraryDesugaringEnabled true,并将coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'加入依赖(示例见 build.gradle.kts):
android { // ··· compileOptions { isCoreLibraryDesugaringEnabled = true // ··· } // ··· } // ··· dependencies { coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5") // ··· }关于脱糖后可用的 Java 11+ API(如
java.nio相关接口),可查阅 Android 开发者文档中"Java 11+ APIs available through desugaring"的兼容性说明。简单来说,脱糖让低版本 Android(SDK 24+)也能运行使用较新 Java API 的 IMA SDK。
第二步:添加依赖与导入
在pubspec.yaml中加入interactive_media_ads与video_player两个插件。仓库中该插件的 pubspec.yaml 显示其当前版本为0.3.0+17,要求 Dart SDK^3.12.0、Flutter>=3.44.0。
然后在 Dart 文件中导入:
import 'package:interactive_media_ads/interactive_media_ads.dart'; import 'package:video_player/video_player.dart';interactive_media_ads.dart作为插件入口 统一导出了全部公开 API,包括五个核心组件类、AdEventType等事件枚举,以及AdError、AdsLoadErrorData、CompanionAdSlotSize等辅助类型。
第三步:创建承载广告与内容的 Widget
创建一个StatefulWidget,负责同时展示广告与播放内容。仓库示例 readme_example.dart 中的AdExampleWidget是一个完整范例,其状态类需要维护以下几类关键对象:
class _AdExampleWidgetState extends State<AdExampleWidget> with WidgetsBindingObserver { // IMA 示例广告标签:前置 + 中插 + 后置的单条 inline 视频广告 static const String _adTagUrl = 'https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/vmap_ad_samples&sz=640x480&cust_params=sample_ar%3Dpremidpost&ciu_szs=300x250&gdfp_req=1&ad_rule=1&output=vmap&unviewed_position_start=1&env=vp&impl=s&cmsid=496&vid=short_onecue&correlator='; // AdsLoader 暴露 requestAds 方法 late final AdsLoader _adsLoader; // AdsManager 控制广告播放并监听广告事件 AdsManager? _adsManager; // 是否应显示内容视频:广告播放期间隐藏内容播放器 bool _shouldShowContentVideo = false; // 控制内容视频播放器 late final VideoPlayerController _contentVideoController; // 周期性向 SDK 上报内容视频播放进度 Timer? _contentProgressTimer; // 向 SDK 提供内容视频当前播放进度,支持中插广告(mid-roll)所必需 final ContentProgressProvider _contentProgressProvider = ContentProgressProvider(); // ··· }代码中的_adTagUrl是 IMA 官方提供的 VMAP 示例广告标签(对应 pre-/mid-/post-roll 三种插播位),实际项目中请替换为你自己的广告标签 URL。广告标签可以是单个 VAST 标签,也可以是 VMAP 或 ad rules 响应,具体取决于广告服务器支持。
第四步:添加广告播放器与内容播放器
实例化用于播放广告的AdDisplayContainer,以及用于播放内容的VideoPlayerController。这是整个接入流程中最核心的一段代码,它串起了AdDisplayContainer → AdsLoader → AdsManager → AdsManagerDelegate的完整调用链:
late final AdDisplayContainer _adDisplayContainer = AdDisplayContainer( onContainerAdded: (AdDisplayContainer container) { _adsLoader = AdsLoader( container: container, onAdsLoaded: (OnAdsLoadedData data) { final AdsManager manager = data.manager; _adsManager = data.manager; manager.setAdsManagerDelegate( AdsManagerDelegate( onAdEvent: (AdEvent event) { debugPrint('OnAdEvent: ${event.type} => ${event.adData}'); switch (event.type) { case AdEventType.loaded: manager.start(); case AdEventType.contentPauseRequested: _pauseContent(); case AdEventType.contentResumeRequested: _resumeContent(); case AdEventType.allAdsCompleted: manager.destroy(); _adsManager = null; case AdEventType.clicked: case AdEventType.complete: case _: } }, onAdErrorEvent: (AdErrorEvent event) { debugPrint('AdErrorEvent: ${event.error.message}'); _resumeContent(); }, ), ); manager.init(settings: AdsRenderingSettings(enablePreloading: true)); }, onAdsLoadError: (AdsLoadErrorData data) { debugPrint('OnAdsLoadError: ${data.error.message}'); _resumeContent(); }, ); // 广告只有在 AdDisplayContainer 被添加到原生 View 层级之后才能被请求 _requestAds(container); }, ); @override void initState() { super.initState(); // ··· _contentVideoController = VideoPlayerController.networkUrl( Uri.parse('https://storage.googleapis.com/gvabox/media/samples/stock.mp4'), ) ..addListener(() { if (_contentVideoController.value.isCompleted) { _adsLoader.contentComplete(); } setState(() {}); }) ..initialize().then((_) { // 确保视频初始化后立即显示首帧,即使在播放按钮被按下之前 setState(() {}); }); }这段代码蕴含了几个必须理解的要点:
onContainerAdded回调是起点:AdDisplayContainer是一个真正的Widget(继承自StatelessWidget,见 ad_display_container.dart),当它的原生视图被挂载到平台视图层级后才会触发onContainerAdded。在容器被加入视图层级之前不能发起广告请求,因此AdsLoader的创建与requestAds调用都放在这个回调里。AdsLoader只实例化一次:如 README 所述,同时只应存在一个AdsLoader,可在页面生命周期内复用。它的构造参数需要container、onAdsLoaded(广告加载成功)、onAdsLoadError(加载失败)三个必填项,另有可选的ImaSettings(通用 SDK 设置)。- 广告成功加载后拿到
AdsManager:onAdsLoaded中的OnAdsLoadedData.manager即本次广告响应的AdsManager。此时需要依次完成:setAdsManagerDelegate(...)注册事件委托 →manager.init(settings: ...)初始化广告体验 → 在loaded事件中manager.start()开始播放。示例中通过AdsRenderingSettings(enablePreloading: true)开启预加载,让 SDK 在init时就加载广告素材(详见后文"渲染设置"小节)。 - 内容播放进度同步:
ContentProgressProvider向 SDK 上报内容视频的播放进度,这是中插广告(mid-roll)按 cue point 触发的前提;同时监听内容播放器完成事件,在播放完毕后调用_adsLoader.contentComplete()通知 SDK 内容已结束(源码见 ads_loader.dart)。
第五步:实现build方法
build返回同时包含广告播放器与内容播放器的 Widget。广告显示容器必须保持常驻屏幕——它既要在广告加载前就存在,也不能在两次广告之间被移除,因为它还负责处理广告点击:
@override Widget build(BuildContext context) { return Scaffold( body: Center( child: SizedBox( width: 300, child: !_contentVideoController.value.isInitialized ? Container() : AspectRatio( aspectRatio: _contentVideoController.value.aspectRatio, child: Stack( children: <Widget>[ // 显示容器必须在广告加载前就出现在屏幕上, // 且不能在广告之间被移除。它负责处理广告点击。 _adDisplayContainer, if (_shouldShowContentVideo) VideoPlayer(_contentVideoController), ], ), ), ), ), floatingActionButton: _contentVideoController.value.isInitialized && _shouldShowContentVideo ? FloatingActionButton( onPressed: () { setState(() { _contentVideoController.value.isPlaying ? _contentVideoController.pause() : _contentVideoController.play(); }); }, child: Icon(_contentVideoController.value.isPlaying ? Icons.pause : Icons.play_arrow), ) : null, ); }布局上,_adDisplayContainer与内容VideoPlayer被放入同一个Stack:广告播放时(_shouldShowContentVideo == false)隐藏内容视频,广告浮层自然覆盖其上;内容播放时再显示VideoPlayer。_shouldShowContentVideo的切换完全由广告事件驱动(见下一节)。
第六步:请求广告与内容播放控制
_requestAds通过AdsLoader.requestAds发起请求,并传入AdsRequest与进度提供器;_resumeContent/_pauseContent则负责在广告打断内容播放时正确切换两个播放器:
Future<void> _requestAds(AdDisplayContainer container) { return _adsLoader.requestAds( AdsRequest(adTagUrl: _adTagUrl, contentProgressProvider: _contentProgressProvider), ); } Future<void> _resumeContent() async { setState(() { _shouldShowContentVideo = true; }); if (_adsManager != null) { _contentProgressTimer = Timer.periodic(const Duration(milliseconds: 200), ( Timer timer, ) async { if (_contentVideoController.value.isInitialized) { final Duration? progress = await _contentVideoController.position; if (progress != null) { await _contentProgressProvider.setProgress( progress: progress, duration: _contentVideoController.value.duration, ); } } }); } await _contentVideoController.play(); } Future<void> _pauseContent() { setState(() { _shouldShowContentVideo = false; }); _contentProgressTimer?.cancel(); _contentProgressTimer = null; return _contentVideoController.pause(); }这里有两个值得注意的细节:
- 进度上报间隔:
Timer.periodic以200ms为间隔调用ContentProgressProvider.setProgress。源码注释明确指出,使用Timer周期性上报时推荐 200ms 间隔(见 content_progress_provider.dart),既保证 cue point 触发的精度,又不会给 UI 线程带来明显负担。 - 恢复内容时的时序:先恢复显示内容视频,再重启进度上报定时器,最后调用
play()。这样广告结束瞬间不会出现黑屏或进度断档。
AdsRequest 参数详解
AdsRequest是定义一次广告请求的对象。除了必填的adTagUrl,源码 ads_request.dart 还暴露了丰富的可选参数:
| 参数 | 类型 | 说明 |
|---|---|---|
adTagUrl | String(必填) | 广告标签 URL,指向 VAST/VMAP/ad rules 广告服务器 |
contentProgressProvider | ContentProgressProvider? | 内容进度提供器,用于按 cue point 调度广告插播(中插广告必需) |
adWillAutoPlay | bool? | 通知 SDK:内容与广告是由用户操作启动还是自动播放 |
adWillPlayMuted | bool? | 通知 SDK:内容与广告是否静音启动 |
continuousPlayback | bool? | 通知 SDK:内容视频是否像电视广播一样连续播放 |
contentDuration | Duration? | 待展示内容的时长 |
contentKeywords | List<String>? | 描述内容的关键词 |
contentTitle | String? | 内容的标题 |
liveStreamPrefetchMaxWaitTime | Duration? | 调用requestAds后、请求广告标签 URL 前的最长等待时间 |
vastLoadTimeout | Duration? | 单个 VAST wrapper 的加载超时时间 |
此外,AdsRequest还提供了AdsRequest.withAdsResponse(...)构造方法,可以直接传入一段canned ads response(预置的 VAST/VMAP/ad rules 响应字符串)代替网络请求——这在测试和离线演示场景中非常实用,二者共享除adTagUrl/adsResponse外的全部参数。
事件驱动模型:AdEventType 与 AdsManager 控制方法
广告的整个生命周期都由事件驱动。示例中AdsManagerDelegate的两个回调(onAdEvent/onAdErrorEvent)覆盖了"广告事件"与"错误事件"两类通知(定义见 ads_manager_delegate.dart)。
AdEventType枚举(完整定义见 platform_ad_event.dart)包含 30 种事件,按用途可归为几类:
- 生命周期核心事件:
loaded(VAST 响应已收到,此时应start())、started、complete、allAdsCompleted(响应中所有有效广告播放完毕,此时应destroy()并置空_adsManager)、skipped、paused、resumed - 内容协同事件:
contentPauseRequested(广告即将覆盖内容,应暂停内容播放)、contentResumeRequested(广告结束,应恢复内容播放) - 广告插播(ad break)事件:
adBreakStarted、adBreakEnded、adBreakReady(VMAP/ad rules 广告插播就绪)、adBreakFetchError(广告插播未能播放任何广告)、adPeriodStarted、adPeriodEnded、cuepointsChanged - 播放进度事件:
firstQuartile、midpoint、thirdQuartile、adProgress(可用来实现倒计时 UI) - 交互事件:
clicked、tapped、iconTapped、iconFallbackImageClosed、skippableStateChanged - 其他:
adBuffering、log、unknown等
每个AdEvent还携带ad(关联的广告对象)与adData(额外的键值数据),方便在事件回调中做埋点或自定义 UI。
AdsManager提供的控制方法(见 ads_loader.dart)覆盖了广告播放的完整控制面:
init({AdsRenderingSettings? settings}):使用默认或自定义渲染设置初始化广告体验start():开始播放广告pause()/resume():暂停/恢复当前广告skip():跳过当前广告(仅在 IMA 未渲染"跳过广告"按钮时生效)discardAdBreak():放弃当前广告插播并恢复内容;若无当前广告则放弃下一个广告插播destroy():停止广告及所有追踪,并释放为播放该广告加载的全部资源adCuePoints(属性):广告插播计划的内容时间偏移列表(单条广告或无插播时为空)
生命周期处理:后台切换时的广告行为
完整的接入还需要处理 App 前后台切换。仓库示例通过WidgetsBindingObserver监听AppLifecycleState变化来暂停/恢复广告(这段逻辑在 README 正文中未展开,但完整存在于 readme_example.dart):
@override void didChangeAppLifecycleState(AppLifecycleState state) { switch (state) { case AppLifecycleState.resumed: if (!_shouldShowContentVideo) { _adsManager?.resume(); } case AppLifecycleState.inactive: // Android 上只能在 inactive 状态暂停广告视频播放器, // 因为它对应 Activity.onPause。该状态在 resume 前也会触发, // 因此只有在 App 即将进入后台时才暂停广告。 if (!_shouldShowContentVideo && _lastLifecycleState == AppLifecycleState.resumed) { _adsManager?.pause(); } case AppLifecycleState.hidden: case AppLifecycleState.paused: case AppLifecycleState.detached: } _lastLifecycleState = state; }要点:仅当当前正在播放广告(_shouldShowContentVideo == false)时才操作_adsManager;且 Android 平台必须在inactive状态暂停广告(对应Activity.onPause时机)。同时别忘记在initState中WidgetsBinding.instance.addObserver(this)、在dispose中removeObserver(this)。
第七步:释放资源
广告与内容播放结束后,需要释放内容播放器并销毁AdsManager:
@override void dispose() { super.dispose(); _contentProgressTimer?.cancel(); _contentVideoController.dispose(); _adsManager?.destroy(); // ··· }dispose中依次:取消进度上报定时器 → 释放VideoPlayerController→ 调用_adsManager?.destroy()销毁广告管理器(释放广告相关资源)。完成这一步,整个接入流程即告结束——正如 README 所说:"That's it! You're now requesting and displaying ads with the IMA SDK."
进阶配置:AdsRenderingSettings 与 Companion Ads
AdsRenderingSettings
AdsRenderingSettings控制广告的渲染行为,构造参数与说明如下(见 ads_rendering_settings.dart):
| 参数 | 默认值 | 说明 |
|---|---|---|
bitrate | null(由 SDK 按当前网速选择) | 最大推荐码率,单位 kbit/s;SDK 会选择低于该值或最接近的媒体 |
enablePreloading | null(平台决定) | 设为true时,SDK 会在AdsManager.init时指示播放器加载广告素材,从而可在start()之前的任意时间点预加载 |
loadVideoTimeout | Duration(seconds: 8) | 媒体加载超时时间(仅适用于 client-side SDK) |
mimeTypes | null(平台决定) | 线性广告视频 MIME 类型优先级列表 |
playAdsAfterTime | null | 仅播放晚于该时间的广告插播;严格晚于,例如设为 15s 会忽略恰好安排在 15s 的插播 |
uiElements | null(平台决定) | SDK 渲染的广告 UI 元素集合;部分广告可能忽略个别元素修改 |
示例中AdsRenderingSettings(enablePreloading: true)即开启广告素材预加载,通常能减少用户等待广告起播的时间。
Companion Ads(伴随广告)
AdDisplayContainer还接受companionSlots参数(Iterable<CompanionAdSlot>)与layoutDirection参数(广告容器内部布局方向,默认TextDirection.ltr),用于在内容页面中渲染与视频广告配对的伴随广告位(如横幅)。CompanionAdSlot及相关尺寸类型(CompanionAdSlotSizeFixed、CompanionAdSlotSizeFluid)均由插件公开导出,相关单元测试见 test/companion_ad_slot_test.dart。
从源码与测试进一步验证
如果你希望深入理解插件的实现,仓库提供了完整的源码与测试作为参考:
- Dart API 层:lib/src 下集中了所有公开类,
platform_interface子目录定义平台抽象接口,android/ios子目录分别持有 Pigeon 生成的双端平台实现(interactive_media_ads.g.dart 与 interactive_media_ads.g.dart) - 原生层:Android 端为 Kotlin 实现(android/src/main/kotlin,含 37 个
.kt文件),iOS 端为 Swift 实现(ios/interactive_media_ads/Sources) - Pigeon 定义:pigeons/interactive_media_ads_android.dart 与 pigeons/interactive_media_ads_ios.dart 是跨平台通道代码的单一事实来源
- 测试覆盖:test 下针对
AdsLoader、AdsManager、AdDisplayContainer、ContentProgressProvider、ImaSettings、CompanionAdSlot等均提供了 Android/iOS 双端单元测试(如 ads_loader_test.dart),另有 integration_test 端到端测试 - 完整可运行示例:example/lib/readme_example.dart(本文所有代码片段即出自于此)、example/lib/video_ad_example_screen.dart 与 example/lib/main.dart
小结
接入interactive_media_ads的完整心智模型可以概括为:一个容器(AdDisplayContainer)负责承载广告视图 → 一个加载器(AdsLoader)负责向 VAST 兼容的广告服务器请求广告 → 一个管理器(AdsManager)持有响应并控制播放 → 一个委托(AdsManagerDelegate)接收事件并驱动内容播放器的暂停/恢复,再辅以ContentProgressProvider上报内容进度以支持中插广告。Android 侧只需完成 Manifest 权限与 Gradle 脱糖两项配置,即可与video_player无缝配合,为你的 Flutter 视频应用接入完整的 pre-roll / mid-roll / post-roll 广告能力。
【免费下载链接】packagesA collection of useful packages maintained by the Flutter team项目地址: https://gitcode.com/GitHub_Trending/pac/packages
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考