diff --git a/packages/interactive_media_ads/CHANGELOG.md b/packages/interactive_media_ads/CHANGELOG.md index 68c213c1c79..a5ac6af7c71 100644 --- a/packages/interactive_media_ads/CHANGELOG.md +++ b/packages/interactive_media_ads/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.2+1 + +* Updates `README` with a usage section and fix app-facing interface documentation. + ## 0.0.2 * Adds Android implementation. diff --git a/packages/interactive_media_ads/README.md b/packages/interactive_media_ads/README.md index 36b725f1979..486f6aa69ec 100644 --- a/packages/interactive_media_ads/README.md +++ b/packages/interactive_media_ads/README.md @@ -4,7 +4,10 @@ Flutter plugin for the [Interactive Media Ads SDKs][1]. [![pub package](https://img.shields.io/pub/v/interactive_media_ads.svg)](https://pub.dev/packages/interactive_media_ads) -A Flutter plugin for using the Interactive Media Ads SDKs on Android and iOS. +IMA SDKs make it easy to integrate multimedia ads into your websites and apps. IMA SDKs can request +ads from any [VAST-compliant][2] ad server and manage ad playback in your apps. With IMA client-side +SDKs, you maintain control of content video playback, while the SDK handles ad playback. Ads play in +a separate video player positioned on top of the app's content video player. | | Android | iOS | |-------------|---------|-------| @@ -12,4 +15,263 @@ A Flutter plugin for using the Interactive Media Ads SDKs on Android and iOS. **This package is still in development.** +## IMA client-side overview + +Implementing IMA client-side involves five main SDK components, which are demonstrated in this +guide: + +* [AdDisplayContainer][3]: A container object where ads are rendered. +* [AdsLoader][4]: Requests ads and handles events from ads request responses. You should only +instantiate one ads loader, which can be reused throughout the life of the application. +* [AdsRequest][5]: An object that defines an ads request. Ads requests specify the URL for the VAST +ad tag, as well as additional parameters, such as ad dimensions. +* [AdsManager][6]: Contains the response to the ads request, controls ad playback, +and listens for ad events fired by the SDK. +* [AdsManagerDelegate][8]: Handles ad events and errors that occur during ad or stream +initialization and playback. + +## Usage + +This guide demonstrates how to integrate the IMA SDK into a new `Widget` using the [video_player][7] +plugin to display content. + +### 1. Add Android Required Permissions + +If building on Android, add the user permissions required by the IMA SDK for requesting ads in +`android/app/src/main/AndroidManifest.xml`. + + +```xml + + + + +``` + +### 2. Add Imports + +Add the import statements for the `interactive_media_ads` and [video_player][7]. Both plugins should +already be added to your `pubspec.yaml`. + + +```dart +import 'package:interactive_media_ads/interactive_media_ads.dart'; +import 'package:video_player/video_player.dart'; +``` + +### 3. Create a New Widget + +Create a new [StatefulWidget](https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html) +that handles displaying Ads and playing content. + + +```dart +/// Example widget displaying an Ad before a video. +class AdExampleWidget extends StatefulWidget { + /// Constructs an [AdExampleWidget]. + const AdExampleWidget({super.key}); + + @override + State createState() => _AdExampleWidgetState(); +} + +class _AdExampleWidgetState extends State { + // IMA sample tag for a single skippable inline video ad. See more IMA sample + // tags at https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/tags + static const String _adTagUrl = + 'https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/single_preroll_skippable&sz=640x480&ciu_szs=300x250%2C728x90&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&impl=s&correlator='; + + // The AdsLoader instance exposes the request ads method. + late final AdsLoader _adsLoader; + + // AdsManager exposes methods to control ad playback and listen to ad events. + AdsManager? _adsManager; + + // Whether the widget should be displaying the content video. The content + // player is hidden while Ads are playing. + bool _shouldShowContentVideo = true; + + // Controls the content video player. + late final VideoPlayerController _contentVideoController; + // ··· + @override + Widget build(BuildContext context) { + // ··· + } +} +``` + +### 4. Add the Video Players + +Instantiate the [AdDisplayContainer][3] for playing Ads and the +[VideoPlayerController](https://pub.dev/documentation/video_player/latest/video_player/VideoPlayerController-class.html) +for playing content. + + +```dart +late final AdDisplayContainer _adDisplayContainer = AdDisplayContainer( + onContainerAdded: (AdDisplayContainer container) { + // Ads can't be requested until the `AdDisplayContainer` has been added to + // the native View hierarchy. + _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((_) { + // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. + setState(() {}); + }); +} +``` + +### 5. Implement the `build` Method + +Return a `Widget` that contains the ad player and the content player. + + +```dart +@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: [ + // The display container must be on screen before any Ads can be + // loaded and can't be removed between ads. This handles clicks for + // ads. + _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, + ); +} +``` + +### 6. Request Ads + +Handle requesting ads and add event listeners to handle when content should be displayed or hidden. + + +```dart +Future _requestAds(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}'); + 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: + } + }, + onAdErrorEvent: (AdErrorEvent event) { + debugPrint('AdErrorEvent: ${event.error.message}'); + _resumeContent(); + }, + )); + + manager.init(); + }, + onAdsLoadError: (AdsLoadErrorData data) { + debugPrint('OnAdsLoadError: ${data.error.message}'); + _resumeContent(); + }, + ); + + return _adsLoader.requestAds(AdsRequest(adTagUrl: _adTagUrl)); +} + +Future _resumeContent() { + setState(() { + _shouldShowContentVideo = true; + }); + return _contentVideoController.play(); +} + +Future _pauseContent() { + setState(() { + _shouldShowContentVideo = false; + }); + return _contentVideoController.pause(); +} +``` + +### 7. Dispose Resources + +Dispose the content player and the destroy the [AdsManager][6]. + + +```dart +@override +void dispose() { + super.dispose(); + _contentVideoController.dispose(); + _adsManager?.destroy(); +} +``` + +That's it! You're now requesting and displaying ads with the IMA SDK. To learn about additional SDK +features, see the [API reference](https://pub.dev/documentation/interactive_media_ads/latest/). + [1]: https://developers.google.com/interactive-media-ads +[2]: https://www.iab.com/guidelines/vast/ +[3]: https://pub.dev/documentation/interactive_media_ads/latest/interactive_media_ads/AdDisplayContainer-class.html +[4]: https://pub.dev/documentation/interactive_media_ads/latest/interactive_media_ads/AdsLoader-class.html +[5]: https://pub.dev/documentation/interactive_media_ads/latest/interactive_media_ads/AdsRequest-class.html +[6]: https://pub.dev/documentation/interactive_media_ads/latest/interactive_media_ads/AdsManager-class.html +[7]: https://pub.dev/packages/video_player +[8]: https://pub.dev/documentation/interactive_media_ads/latest/interactive_media_ads/AdsManagerDelegate-class.html diff --git a/packages/interactive_media_ads/android/src/main/kotlin/dev/flutter/packages/interactive_media_ads/AdsRequestProxyApi.kt b/packages/interactive_media_ads/android/src/main/kotlin/dev/flutter/packages/interactive_media_ads/AdsRequestProxyApi.kt index 9222ea80d40..0f6746fe1c2 100644 --- a/packages/interactive_media_ads/android/src/main/kotlin/dev/flutter/packages/interactive_media_ads/AdsRequestProxyApi.kt +++ b/packages/interactive_media_ads/android/src/main/kotlin/dev/flutter/packages/interactive_media_ads/AdsRequestProxyApi.kt @@ -21,7 +21,7 @@ class AdsRequestProxyApi(override val pigeonRegistrar: ProxyApiRegistrar) : * * This must match the version in pubspec.yaml. */ - const val pluginVersion = "0.0.2" + const val pluginVersion = "0.0.2+1" } override fun setAdTagUrl(pigeon_instance: AdsRequest, adTagUrl: String) { diff --git a/packages/interactive_media_ads/example/android/app/src/main/AndroidManifest.xml b/packages/interactive_media_ads/example/android/app/src/main/AndroidManifest.xml index 73dc642f81e..48c5bc7ca18 100644 --- a/packages/interactive_media_ads/example/android/app/src/main/AndroidManifest.xml +++ b/packages/interactive_media_ads/example/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,9 @@ + - + { + // IMA sample tag for a single skippable inline video ad. See more IMA sample + // tags at https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/tags + static const String _adTagUrl = + 'https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/single_preroll_skippable&sz=640x480&ciu_szs=300x250%2C728x90&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&impl=s&correlator='; + + // The AdsLoader instance exposes the request ads method. late final AdsLoader _adsLoader; + + // AdsManager exposes methods to control ad playback and listen to ad events. AdsManager? _adsManager; + + // Whether the widget should be displaying the content video. The content + // player is hidden while Ads are playing. bool _shouldShowContentVideo = true; + // Controls the content video player. late final VideoPlayerController _contentVideoController; + // #enddocregion example_widget + // #docregion ad_and_content_players late final AdDisplayContainer _adDisplayContainer = AdDisplayContainer( onContainerAdded: (AdDisplayContainer container) { + // Ads can't be requested until the `AdDisplayContainer` has been added to + // the native View hierarchy. _requestAds(container); }, ); @@ -75,21 +89,9 @@ class _AdExampleWidgetState extends State { setState(() {}); }); } + // #enddocregion ad_and_content_players - Future _resumeContent() { - setState(() { - _shouldShowContentVideo = true; - }); - return _contentVideoController.play(); - } - - Future _pauseContent() { - setState(() { - _shouldShowContentVideo = false; - }); - return _contentVideoController.pause(); - } - + // #docregion request_ads Future _requestAds(AdDisplayContainer container) { _adsLoader = AdsLoader( container: container, @@ -131,15 +133,35 @@ class _AdExampleWidgetState extends State { return _adsLoader.requestAds(AdsRequest(adTagUrl: _adTagUrl)); } + Future _resumeContent() { + setState(() { + _shouldShowContentVideo = true; + }); + return _contentVideoController.play(); + } + + Future _pauseContent() { + setState(() { + _shouldShowContentVideo = false; + }); + return _contentVideoController.pause(); + } + // #enddocregion request_ads + + // #docregion dispose @override void dispose() { super.dispose(); _contentVideoController.dispose(); _adsManager?.destroy(); } + // #enddocregion dispose + // #docregion example_widget + // #docregion widget_build @override Widget build(BuildContext context) { + // #enddocregion example_widget return Scaffold( body: Center( child: SizedBox( @@ -179,5 +201,8 @@ class _AdExampleWidgetState extends State { ) : null, ); + // #docregion example_widget } + // #enddocregion widget_build } +// #enddocregion example_widget diff --git a/packages/interactive_media_ads/lib/src/ad_display_container.dart b/packages/interactive_media_ads/lib/src/ad_display_container.dart index 99da19682b8..cc254a33e51 100644 --- a/packages/interactive_media_ads/lib/src/ad_display_container.dart +++ b/packages/interactive_media_ads/lib/src/ad_display_container.dart @@ -7,7 +7,7 @@ import 'package:flutter/cupertino.dart'; import 'platform_interface/platform_ad_display_container.dart'; import 'platform_interface/platform_interface.dart'; -/// Handles playing ads after they've been received from the server. +/// A [Widget] for displaying loaded ads. /// /// ## Platform-Specific Features /// This class contains an underlying implementation provided by the current diff --git a/packages/interactive_media_ads/lib/src/ads_loader.dart b/packages/interactive_media_ads/lib/src/ads_loader.dart index 9370cc9fec3..4ab3a97c147 100644 --- a/packages/interactive_media_ads/lib/src/ads_loader.dart +++ b/packages/interactive_media_ads/lib/src/ads_loader.dart @@ -8,7 +8,8 @@ import 'ad_display_container.dart'; import 'ads_manager_delegate.dart'; import 'platform_interface/platform_interface.dart'; -/// Handles playing ads after they've been received from the server. +/// Allows publishers to request ads from ad servers or a dynamic ad insertion +/// stream. /// /// ## Platform-Specific Features /// This class contains an underlying implementation provided by the current @@ -92,6 +93,9 @@ class AdsLoader { } /// Requests ads from a server. + /// + /// Ads cannot be requested until the `AdDisplayContainer` has been added to + /// the native View hierarchy. See [AdDisplayContainer.onContainerAdded]. Future requestAds(AdsRequest request) { return platform.requestAds(request); } diff --git a/packages/interactive_media_ads/lib/src/ads_manager_delegate.dart b/packages/interactive_media_ads/lib/src/ads_manager_delegate.dart index 4a3813c719a..c04cf5c9c52 100644 --- a/packages/interactive_media_ads/lib/src/ads_manager_delegate.dart +++ b/packages/interactive_media_ads/lib/src/ads_manager_delegate.dart @@ -4,7 +4,8 @@ import 'platform_interface/platform_interface.dart'; -/// Handles playing ads after they've been received from the server. +/// Delegate for ad events or errors that occur during ad or stream +/// initialization and playback. /// /// ## Platform-Specific Features /// This class contains an underlying implementation provided by the current diff --git a/packages/interactive_media_ads/pubspec.yaml b/packages/interactive_media_ads/pubspec.yaml index c3bc1e75b17..99fa8785dc8 100644 --- a/packages/interactive_media_ads/pubspec.yaml +++ b/packages/interactive_media_ads/pubspec.yaml @@ -2,7 +2,7 @@ name: interactive_media_ads description: A Flutter plugin for using the Interactive Media Ads SDKs on Android and iOS. repository: https://github.com/flutter/packages/tree/main/packages/interactive_media_ads issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+interactive_media_ads%22 -version: 0.0.2 # This must match the version in `android/src/main/kotlin/dev/flutter/packages/interactive_media_ads/AdsRequestProxyApi.kt` +version: 0.0.2+1 # This must match the version in `android/src/main/kotlin/dev/flutter/packages/interactive_media_ads/AdsRequestProxyApi.kt` environment: sdk: ^3.2.3