Skip to content

Commit a0ef40e

Browse files
[interactive_media_ads] Adds support for pausing and resuming Ad playback and skipping an Ad (flutter#7285)
Fixes flutter#152255 Also adds `skip` and `discardAdBreak` to `AdsManager`. Also changes Android to not automatically start playing an Ad when the `VideoView` is prepared. `onPrepared` is called automatically whenever an app returns to the foreground by requiring the app to call `AdsManager.resume` to continue Ad playback after returning to the foreground. This makes Android consistent with iOS.
1 parent e17304d commit a0ef40e

22 files changed

+465
-24
lines changed

packages/interactive_media_ads/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 0.2.0
2+
3+
* Adds support for pausing and resuming Ad playback. See `AdsManager.pause` and `AdsManager.resume`.
4+
* Adds support to skip an Ad. See `AdsManager.skip` and `AdsManager.discardAdBreak`.
5+
* **Breaking Change** To keep platform consistency, Android no longer continues playing an Ad
6+
whenever it returns from an Ad click. Call `AdsManager.resume` to resume Ad playback.
7+
18
## 0.1.2+6
29

310
* Fixes bug where the ad would play when the app returned to foreground during content playback.

packages/interactive_media_ads/CONTRIBUTING.md

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ platform classes that are returned by this.
112112
#### SDK Wrappers
113113

114114
The platform implementations use Dart wrappers of their native SDKs. The SDKs are wrapped using
115-
using the `pigeon` package. However, the code that handles generating the wrappers are still in the
116-
process of review, so this plugin must use a git dependency in the pubspec.
115+
using the `pigeon` package. However, the code that handles generating the wrappers for iOS is still
116+
in the process of review, so this plugin must use a git dependency in the pubspec.
117117

118118
The wrappers for the SDK of each platform can be updated and modified by changing the pigeon files:
119119

@@ -135,16 +135,12 @@ To update a wrapper for a platform, follow the steps:
135135
* Android: Run `flutter build apk --debug` in `example/`.
136136
* iOS: Run `flutter build ios --simulator` in `example/`
137137

138-
##### 2. Add the correct `pigeon` package to `dev_dependencies` in the `pubspec.yaml` and run `pub upgrade`
138+
##### 2. Ensure the correct `pigeon` package is added to `dev_dependencies` in the `pubspec.yaml` and run `pub upgrade`
139139

140140
Android:
141141

142142
```yaml
143-
pigeon:
144-
git:
145-
url: git@github.com:bparrishMines/packages.git
146-
ref: pigeon_kotlin_split
147-
path: packages/pigeon
143+
pigeon: ^22.2.0
148144
```
149145
150146
iOS:

packages/interactive_media_ads/README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ class AdExampleWidget extends StatefulWidget {
7979
State<AdExampleWidget> createState() => _AdExampleWidgetState();
8080
}
8181
82-
class _AdExampleWidgetState extends State<AdExampleWidget> {
82+
class _AdExampleWidgetState extends State<AdExampleWidget>
83+
with WidgetsBindingObserver {
8384
// IMA sample tag for a single skippable inline video ad. See more IMA sample
8485
// tags at https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/tags
8586
static const String _adTagUrl =
@@ -91,6 +92,7 @@ class _AdExampleWidgetState extends State<AdExampleWidget> {
9192
// AdsManager exposes methods to control ad playback and listen to ad events.
9293
AdsManager? _adsManager;
9394
95+
// ···
9496
// Whether the widget should be displaying the content video. The content
9597
// player is hidden while Ads are playing.
9698
bool _shouldShowContentVideo = true;
@@ -124,6 +126,7 @@ late final AdDisplayContainer _adDisplayContainer = AdDisplayContainer(
124126
@override
125127
void initState() {
126128
super.initState();
129+
// ···
127130
_contentVideoController = VideoPlayerController.networkUrl(
128131
Uri.parse(
129132
'https://storage.googleapis.com/gvabox/media/samples/stock.mp4',
@@ -132,8 +135,8 @@ void initState() {
132135
..addListener(() {
133136
if (_contentVideoController.value.isCompleted) {
134137
_adsLoader.contentComplete();
135-
setState(() {});
136138
}
139+
setState(() {});
137140
})
138141
..initialize().then((_) {
139142
// Ensure the first frame is shown after the video is initialized, even before the play button has been pressed.
@@ -257,7 +260,7 @@ Future<void> _pauseContent() {
257260

258261
### 7. Dispose Resources
259262

260-
Dispose the content player and the destroy the [AdsManager][6].
263+
Dispose the content player and destroy the [AdsManager][6].
261264

262265
<?code-excerpt "example/lib/main.dart (dispose)"?>
263266
```dart
@@ -266,6 +269,7 @@ void dispose() {
266269
super.dispose();
267270
_contentVideoController.dispose();
268271
_adsManager?.destroy();
272+
// ···
269273
}
270274
```
271275

packages/interactive_media_ads/android/src/main/kotlin/dev/flutter/packages/interactive_media_ads/AdsRequestProxyApi.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class AdsRequestProxyApi(override val pigeonRegistrar: ProxyApiRegistrar) :
2121
*
2222
* This must match the version in pubspec.yaml.
2323
*/
24-
const val pluginVersion = "0.1.2+6"
24+
const val pluginVersion = "0.2.0"
2525
}
2626

2727
override fun setAdTagUrl(pigeon_instance: AdsRequest, adTagUrl: String) {

packages/interactive_media_ads/android/src/main/kotlin/dev/flutter/packages/interactive_media_ads/InteractiveMediaAdsLibrary.g.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2013 The Flutter Authors. All rights reserved.
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
4-
// Autogenerated from Pigeon (v22.1.0), do not edit directly.
4+
// Autogenerated from Pigeon (v22.2.0), do not edit directly.
55
// See also: https://pub.dev/packages/pigeon
66
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass", "SyntheticAccessor")
77

packages/interactive_media_ads/example/lib/main.dart

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ class AdExampleWidget extends StatefulWidget {
3232
State<AdExampleWidget> createState() => _AdExampleWidgetState();
3333
}
3434

35-
class _AdExampleWidgetState extends State<AdExampleWidget> {
35+
class _AdExampleWidgetState extends State<AdExampleWidget>
36+
with WidgetsBindingObserver {
3637
// IMA sample tag for a single skippable inline video ad. See more IMA sample
3738
// tags at https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/tags
3839
static const String _adTagUrl =
@@ -44,6 +45,11 @@ class _AdExampleWidgetState extends State<AdExampleWidget> {
4445
// AdsManager exposes methods to control ad playback and listen to ad events.
4546
AdsManager? _adsManager;
4647

48+
// #enddocregion example_widget
49+
// Last state received in `didChangeAppLifecycleState`.
50+
AppLifecycleState _lastLifecycleState = AppLifecycleState.resumed;
51+
52+
// #docregion example_widget
4753
// Whether the widget should be displaying the content video. The content
4854
// player is hidden while Ads are playing.
4955
bool _shouldShowContentVideo = true;
@@ -64,6 +70,11 @@ class _AdExampleWidgetState extends State<AdExampleWidget> {
6470
@override
6571
void initState() {
6672
super.initState();
73+
// #enddocregion ad_and_content_players
74+
// Adds this instance as an observer for `AppLifecycleState` changes.
75+
WidgetsBinding.instance.addObserver(this);
76+
77+
// #docregion ad_and_content_players
6778
_contentVideoController = VideoPlayerController.networkUrl(
6879
Uri.parse(
6980
'https://storage.googleapis.com/gvabox/media/samples/stock.mp4',
@@ -72,8 +83,8 @@ class _AdExampleWidgetState extends State<AdExampleWidget> {
7283
..addListener(() {
7384
if (_contentVideoController.value.isCompleted) {
7485
_adsLoader.contentComplete();
75-
setState(() {});
7686
}
87+
setState(() {});
7788
})
7889
..initialize().then((_) {
7990
// Ensure the first frame is shown after the video is initialized, even before the play button has been pressed.
@@ -82,6 +93,29 @@ class _AdExampleWidgetState extends State<AdExampleWidget> {
8293
}
8394
// #enddocregion ad_and_content_players
8495

96+
@override
97+
void didChangeAppLifecycleState(AppLifecycleState state) {
98+
switch (state) {
99+
case AppLifecycleState.resumed:
100+
if (!_shouldShowContentVideo) {
101+
_adsManager?.resume();
102+
}
103+
case AppLifecycleState.inactive:
104+
// Pausing the Ad video player on Android can only be done in this state
105+
// because it corresponds to `Activity.onPause`. This state is also
106+
// triggered before resume, so this will only pause the Ad if the app is
107+
// in the process of being sent to the background.
108+
if (!_shouldShowContentVideo &&
109+
_lastLifecycleState == AppLifecycleState.resumed) {
110+
_adsManager?.pause();
111+
}
112+
case AppLifecycleState.hidden:
113+
case AppLifecycleState.paused:
114+
case AppLifecycleState.detached:
115+
}
116+
_lastLifecycleState = state;
117+
}
118+
85119
// #docregion request_ads
86120
Future<void> _requestAds(AdDisplayContainer container) {
87121
_adsLoader = AdsLoader(
@@ -146,6 +180,9 @@ class _AdExampleWidgetState extends State<AdExampleWidget> {
146180
super.dispose();
147181
_contentVideoController.dispose();
148182
_adsManager?.destroy();
183+
// #enddocregion dispose
184+
WidgetsBinding.instance.removeObserver(this);
185+
// #docregion dispose
149186
}
150187
// #enddocregion dispose
151188

packages/interactive_media_ads/ios/interactive_media_ads/Sources/interactive_media_ads/AdsRequestProxyAPIDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class AdsRequestProxyAPIDelegate: PigeonApiDelegateIMAAdsRequest {
1313
/// The current version of the `interactive_media_ads` plugin.
1414
///
1515
/// This must match the version in pubspec.yaml.
16-
static let pluginVersion = "0.1.2+6"
16+
static let pluginVersion = "0.2.0"
1717

1818
func pigeonDefaultConstructor(
1919
pigeonApi: PigeonApiIMAAdsRequest, adTagUrl: String, adDisplayContainer: IMAAdDisplayContainer,

packages/interactive_media_ads/lib/src/ads_loader.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,30 @@ class AdsManager {
158158
return platform.setAdsManagerDelegate(delegate.platform);
159159
}
160160

161+
/// Pauses the current ad.
162+
Future<void> pause() {
163+
return platform.pause();
164+
}
165+
166+
/// Resumes the current ad.
167+
Future<void> resume() {
168+
return platform.resume();
169+
}
170+
171+
/// Skips the current ad.
172+
///
173+
/// This only skips ads if IMA does not render the 'Skip ad' button.
174+
Future<void> skip() {
175+
return platform.skip();
176+
}
177+
178+
/// Discards current ad break and resumes content.
179+
///
180+
/// If there is no current ad then the next ad break is discarded.
181+
Future<void> discardAdBreak() {
182+
return platform.discardAdBreak();
183+
}
184+
161185
/// Stops the ad and all tracking, then releases all assets that were loaded
162186
/// to play the ad.
163187
Future<void> destroy() {

packages/interactive_media_ads/lib/src/android/android_ad_display_container.dart

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ base class AndroidAdDisplayContainer extends PlatformAdDisplayContainer {
120120

121121
int? _adDuration;
122122

123+
// Whether MediaPlayer.start() should be called whenever the VideoView
124+
// `onPrepared` callback is triggered. `onPrepared` is triggered whenever the
125+
// app is resumed after being inactive.
126+
bool _startPlayerWhenVideoIsPrepared = true;
127+
123128
late final AndroidAdDisplayContainerCreationParams _androidParams =
124129
params is AndroidAdDisplayContainerCreationParams
125130
? params as AndroidAdDisplayContainerCreationParams
@@ -217,10 +222,12 @@ base class AndroidAdDisplayContainer extends PlatformAdDisplayContainer {
217222
if (container._savedAdPosition > 0) {
218223
await player.seekTo(container._savedAdPosition);
219224
}
220-
}
221225

222-
await player.start();
223-
container?._startAdProgressTracking();
226+
if (container._startPlayerWhenVideoIsPrepared) {
227+
await player.start();
228+
container._startAdProgressTracking();
229+
}
230+
}
224231
},
225232
onError: (_, __, ___, ____) {
226233
final AndroidAdDisplayContainer? container = weakThis.target;
@@ -256,24 +263,33 @@ base class AndroidAdDisplayContainer extends PlatformAdDisplayContainer {
256263
pauseAd: (_, __) async {
257264
final AndroidAdDisplayContainer? container = weakThis.target;
258265
if (container != null) {
266+
// Setting this to false ensures the ad doesn't start playing if an
267+
// app is returned to the foreground.
268+
container._startPlayerWhenVideoIsPrepared = false;
259269
await container._mediaPlayer!.pause();
260270
container._savedAdPosition =
261271
await container._videoView.getCurrentPosition();
262272
container._stopAdProgressTracking();
263273
}
264274
},
265275
playAd: (_, ima.AdMediaInfo adMediaInfo) {
266-
weakThis.target?._videoView.setVideoUri(adMediaInfo.url);
276+
final AndroidAdDisplayContainer? container = weakThis.target;
277+
if (container != null) {
278+
container._startPlayerWhenVideoIsPrepared = true;
279+
container._videoView.setVideoUri(adMediaInfo.url);
280+
}
267281
},
268282
release: (_) {},
269283
stopAd: (_, __) {
270284
final AndroidAdDisplayContainer? container = weakThis.target;
271285
if (container != null) {
286+
// Clear and reset all state.
272287
container._stopAdProgressTracking();
273288
container._videoView.setVideoUri(null);
274289
container._clearMediaPlayer();
275290
container._loadedAdMediaInfo = null;
276291
container._adDuration = null;
292+
container._startPlayerWhenVideoIsPrepared = true;
277293
}
278294
},
279295
);

packages/interactive_media_ads/lib/src/android/android_ads_manager.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,26 @@ class AndroidAdsManager extends PlatformAdsManager {
4949
return _manager.start();
5050
}
5151

52+
@override
53+
Future<void> discardAdBreak() {
54+
return _manager.discardAdBreak();
55+
}
56+
57+
@override
58+
Future<void> pause() {
59+
return _manager.pause();
60+
}
61+
62+
@override
63+
Future<void> resume() {
64+
return _manager.resume();
65+
}
66+
67+
@override
68+
Future<void> skip() {
69+
return _manager.skip();
70+
}
71+
5272
// This value is created in a static method because the callback methods for
5373
// any wrapped classes must not reference the encapsulating object. This is to
5474
// prevent a circular reference that prevents garbage collection.

0 commit comments

Comments
 (0)