interactive_media_ads 接入实战:在 Flutter 中集成 IMA SDK 播放 VAST 视频广告
2026/9/18 4:37:34 网站建设 项目流程

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."

平台支持情况

平台支持版本
AndroidSDK 24+
iOS13.0+

注意(README 原文声明):Background Audio ads(后台音频广告)与 Google Dynamic Ad Insertion(Google 动态广告插入)方法目前不受支持

五大核心组件:IMA client-side 的骨架

README 指出,实现 IMA client-side 涉及五个主要 SDK 组件,它们也是本插件 Dart API 的核心类:

组件Dart 类职责
AdDisplayContainerad_display_container.dart广告渲染的容器 Widget,负责承载广告视图并处理广告点击
AdsLoaderads_loader.dart请求广告并处理广告请求响应的各类事件;同一时间只能实例化一个 AdsLoader,可在页面生命周期内复用
AdsRequestads_request.dart定义一次广告请求:指定 VAST 广告标签(ad tag)URL、广告尺寸等附加参数
AdsManagerads_loader.dart(同文件)持有广告请求的响应结果,控制广告播放,并监听 SDK 抛出的广告事件
AdsManagerDelegateads_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_adsvideo_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等事件枚举,以及AdErrorAdsLoadErrorDataCompanionAdSlotSize等辅助类型。

第三步:创建承载广告与内容的 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,可在页面生命周期内复用。它的构造参数需要containeronAdsLoaded(广告加载成功)、onAdsLoadError(加载失败)三个必填项,另有可选的ImaSettings(通用 SDK 设置)。
  • 广告成功加载后拿到AdsManageronAdsLoaded中的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.periodic200ms为间隔调用ContentProgressProvider.setProgress。源码注释明确指出,使用Timer周期性上报时推荐 200ms 间隔(见 content_progress_provider.dart),既保证 cue point 触发的精度,又不会给 UI 线程带来明显负担。
  • 恢复内容时的时序:先恢复显示内容视频,再重启进度上报定时器,最后调用play()。这样广告结束瞬间不会出现黑屏或进度断档。

AdsRequest 参数详解

AdsRequest是定义一次广告请求的对象。除了必填的adTagUrl,源码 ads_request.dart 还暴露了丰富的可选参数:

参数类型说明
adTagUrlString(必填)广告标签 URL,指向 VAST/VMAP/ad rules 广告服务器
contentProgressProviderContentProgressProvider?内容进度提供器,用于按 cue point 调度广告插播(中插广告必需)
adWillAutoPlaybool?通知 SDK:内容与广告是由用户操作启动还是自动播放
adWillPlayMutedbool?通知 SDK:内容与广告是否静音启动
continuousPlaybackbool?通知 SDK:内容视频是否像电视广播一样连续播放
contentDurationDuration?待展示内容的时长
contentKeywordsList<String>?描述内容的关键词
contentTitleString?内容的标题
liveStreamPrefetchMaxWaitTimeDuration?调用requestAds后、请求广告标签 URL 前的最长等待时间
vastLoadTimeoutDuration?单个 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())、startedcompleteallAdsCompleted(响应中所有有效广告播放完毕,此时应destroy()并置空_adsManager)、skippedpausedresumed
  • 内容协同事件contentPauseRequested(广告即将覆盖内容,应暂停内容播放)、contentResumeRequested(广告结束,应恢复内容播放)
  • 广告插播(ad break)事件adBreakStartedadBreakEndedadBreakReady(VMAP/ad rules 广告插播就绪)、adBreakFetchError(广告插播未能播放任何广告)、adPeriodStartedadPeriodEndedcuepointsChanged
  • 播放进度事件firstQuartilemidpointthirdQuartileadProgress(可用来实现倒计时 UI)
  • 交互事件clickedtappediconTappediconFallbackImageClosedskippableStateChanged
  • 其他adBufferinglogunknown

每个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时机)。同时别忘记在initStateWidgetsBinding.instance.addObserver(this)、在disposeremoveObserver(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):

参数默认值说明
bitratenull(由 SDK 按当前网速选择)最大推荐码率,单位 kbit/s;SDK 会选择低于该值或最接近的媒体
enablePreloadingnull(平台决定)设为true时,SDK 会在AdsManager.init时指示播放器加载广告素材,从而可在start()之前的任意时间点预加载
loadVideoTimeoutDuration(seconds: 8)媒体加载超时时间(仅适用于 client-side SDK)
mimeTypesnull(平台决定)线性广告视频 MIME 类型优先级列表
playAdsAfterTimenull仅播放晚于该时间的广告插播;严格晚于,例如设为 15s 会忽略恰好安排在 15s 的插播
uiElementsnull(平台决定)SDK 渲染的广告 UI 元素集合;部分广告可能忽略个别元素修改

示例中AdsRenderingSettings(enablePreloading: true)即开启广告素材预加载,通常能减少用户等待广告起播的时间。

Companion Ads(伴随广告)

AdDisplayContainer还接受companionSlots参数(Iterable<CompanionAdSlot>)与layoutDirection参数(广告容器内部布局方向,默认TextDirection.ltr),用于在内容页面中渲染与视频广告配对的伴随广告位(如横幅)。CompanionAdSlot及相关尺寸类型(CompanionAdSlotSizeFixedCompanionAdSlotSizeFluid)均由插件公开导出,相关单元测试见 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 下针对AdsLoaderAdsManagerAdDisplayContainerContentProgressProviderImaSettingsCompanionAdSlot等均提供了 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询