Skip to content

Commit c7406b3

Browse files
[interactive_media_ads] Adds support for mid-roll ads (#7407)
Adds `ContentProgressProvider` and adds `AdsRequest.contentProgressProvider` field. This changes the platform interface `AdsRequest` to `PlatformAdsRequest`, so the `PlatformContentProgressProvider` can be passed to it. And the app-facing `AdsRequest` can take a `ContentProgressProver`. Fixes flutter/flutter#154261
1 parent 4f2b9cd commit c7406b3

37 files changed

+910
-44
lines changed

packages/interactive_media_ads/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.2.2
2+
3+
* Adds support for mid-roll ads. See `AdsRequest.contentProgressProvider`.
4+
15
## 0.2.1
26

37
* Adds internal wrapper for Android native `ContentProgressProvider`.

packages/interactive_media_ads/README.md

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ class AdExampleWidget extends StatefulWidget {
8181
8282
class _AdExampleWidgetState extends State<AdExampleWidget>
8383
with WidgetsBindingObserver {
84-
// IMA sample tag for a single skippable inline video ad. See more IMA sample
84+
// IMA sample tag for a pre-, mid-, and post-roll, single inline video ad. See more IMA sample
8585
// tags at https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/tags
8686
static const String _adTagUrl =
87-
'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=';
87+
'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=';
8888
8989
// The AdsLoader instance exposes the request ads method.
9090
late final AdsLoader _adsLoader;
@@ -99,6 +99,15 @@ class _AdExampleWidgetState extends State<AdExampleWidget>
9999
100100
// Controls the content video player.
101101
late final VideoPlayerController _contentVideoController;
102+
103+
// Periodically updates the SDK of the current playback progress of the
104+
// content video.
105+
Timer? _contentProgressTimer;
106+
107+
// Provides the SDK with the current playback progress of the content video.
108+
// This is required to support mid-roll ads.
109+
final ContentProgressProvider _contentProgressProvider =
110+
ContentProgressProvider();
102111
// ···
103112
@override
104113
Widget build(BuildContext context) {
@@ -240,20 +249,43 @@ Future<void> _requestAds(AdDisplayContainer container) {
240249
},
241250
);
242251
243-
return _adsLoader.requestAds(AdsRequest(adTagUrl: _adTagUrl));
252+
return _adsLoader.requestAds(AdsRequest(
253+
adTagUrl: _adTagUrl,
254+
contentProgressProvider: _contentProgressProvider,
255+
));
244256
}
245257
246-
Future<void> _resumeContent() {
258+
Future<void> _resumeContent() async {
247259
setState(() {
248260
_shouldShowContentVideo = true;
249261
});
250-
return _contentVideoController.play();
262+
263+
if (_adsManager != null) {
264+
_contentProgressTimer = Timer.periodic(
265+
const Duration(milliseconds: 200),
266+
(Timer timer) async {
267+
if (_contentVideoController.value.isInitialized) {
268+
final Duration? progress = await _contentVideoController.position;
269+
if (progress != null) {
270+
await _contentProgressProvider.setProgress(
271+
progress: progress,
272+
duration: _contentVideoController.value.duration,
273+
);
274+
}
275+
}
276+
},
277+
);
278+
}
279+
280+
await _contentVideoController.play();
251281
}
252282
253283
Future<void> _pauseContent() {
254284
setState(() {
255285
_shouldShowContentVideo = false;
256286
});
287+
_contentProgressTimer?.cancel();
288+
_contentProgressTimer = null;
257289
return _contentVideoController.pause();
258290
}
259291
```
@@ -267,6 +299,7 @@ Dispose the content player and destroy the [AdsManager][6].
267299
@override
268300
void dispose() {
269301
super.dispose();
302+
_contentProgressTimer?.cancel();
270303
_contentVideoController.dispose();
271304
_adsManager?.destroy();
272305
// ···

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.2.1"
24+
const val pluginVersion = "0.2.2"
2525
}
2626

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

packages/interactive_media_ads/example/lib/main.dart

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ class AdExampleWidget extends StatefulWidget {
3434

3535
class _AdExampleWidgetState extends State<AdExampleWidget>
3636
with WidgetsBindingObserver {
37-
// IMA sample tag for a single skippable inline video ad. See more IMA sample
37+
// IMA sample tag for a pre-, mid-, and post-roll, single inline video ad. See more IMA sample
3838
// tags at https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/tags
3939
static const String _adTagUrl =
40-
'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=';
40+
'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=';
4141

4242
// The AdsLoader instance exposes the request ads method.
4343
late final AdsLoader _adsLoader;
@@ -56,6 +56,15 @@ class _AdExampleWidgetState extends State<AdExampleWidget>
5656

5757
// Controls the content video player.
5858
late final VideoPlayerController _contentVideoController;
59+
60+
// Periodically updates the SDK of the current playback progress of the
61+
// content video.
62+
Timer? _contentProgressTimer;
63+
64+
// Provides the SDK with the current playback progress of the content video.
65+
// This is required to support mid-roll ads.
66+
final ContentProgressProvider _contentProgressProvider =
67+
ContentProgressProvider();
5968
// #enddocregion example_widget
6069

6170
// #docregion ad_and_content_players
@@ -156,20 +165,43 @@ class _AdExampleWidgetState extends State<AdExampleWidget>
156165
},
157166
);
158167

159-
return _adsLoader.requestAds(AdsRequest(adTagUrl: _adTagUrl));
168+
return _adsLoader.requestAds(AdsRequest(
169+
adTagUrl: _adTagUrl,
170+
contentProgressProvider: _contentProgressProvider,
171+
));
160172
}
161173

162-
Future<void> _resumeContent() {
174+
Future<void> _resumeContent() async {
163175
setState(() {
164176
_shouldShowContentVideo = true;
165177
});
166-
return _contentVideoController.play();
178+
179+
if (_adsManager != null) {
180+
_contentProgressTimer = Timer.periodic(
181+
const Duration(milliseconds: 200),
182+
(Timer timer) async {
183+
if (_contentVideoController.value.isInitialized) {
184+
final Duration? progress = await _contentVideoController.position;
185+
if (progress != null) {
186+
await _contentProgressProvider.setProgress(
187+
progress: progress,
188+
duration: _contentVideoController.value.duration,
189+
);
190+
}
191+
}
192+
},
193+
);
194+
}
195+
196+
await _contentVideoController.play();
167197
}
168198

169199
Future<void> _pauseContent() {
170200
setState(() {
171201
_shouldShowContentVideo = false;
172202
});
203+
_contentProgressTimer?.cancel();
204+
_contentProgressTimer = null;
173205
return _contentVideoController.pause();
174206
}
175207
// #enddocregion request_ads
@@ -178,6 +210,7 @@ class _AdExampleWidgetState extends State<AdExampleWidget>
178210
@override
179211
void dispose() {
180212
super.dispose();
213+
_contentProgressTimer?.cancel();
181214
_contentVideoController.dispose();
182215
_adsManager?.destroy();
183216
// #enddocregion dispose

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.2.1"
16+
static let pluginVersion = "0.2.2"
1717

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

packages/interactive_media_ads/lib/interactive_media_ads.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
export 'src/ad_display_container.dart';
66
export 'src/ads_loader.dart';
77
export 'src/ads_manager_delegate.dart';
8+
export 'src/ads_request.dart';
89
export 'src/android/android_interactive_media_ads.dart'
910
show AndroidInteractiveMediaAds;
11+
export 'src/content_progress_provider.dart';
1012
export 'src/ios/ios_interactive_media_ads.dart' show IOSInteractiveMediaAds;
1113
export 'src/platform_interface/platform_interface.dart'
1214
show
@@ -16,5 +18,4 @@ export 'src/platform_interface/platform_interface.dart'
1618
AdErrorType,
1719
AdEvent,
1820
AdEventType,
19-
AdsLoadErrorData,
20-
AdsRequest;
21+
AdsLoadErrorData;

packages/interactive_media_ads/lib/src/ads_loader.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
66

77
import 'ad_display_container.dart';
88
import 'ads_manager_delegate.dart';
9+
import 'ads_request.dart';
910
import 'platform_interface/platform_interface.dart';
1011

1112
/// Allows publishers to request ads from ad servers or a dynamic ad insertion
@@ -97,7 +98,7 @@ class AdsLoader {
9798
/// Ads cannot be requested until the `AdDisplayContainer` has been added to
9899
/// the native View hierarchy. See [AdDisplayContainer.onContainerAdded].
99100
Future<void> requestAds(AdsRequest request) {
100-
return platform.requestAds(request);
101+
return platform.requestAds(request.platform);
101102
}
102103
}
103104

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'content_progress_provider.dart';
6+
import 'platform_interface/platform_interface.dart';
7+
8+
/// An object containing the data used to request ads from the server.
9+
class AdsRequest {
10+
/// Creates an [AdsRequest].
11+
AdsRequest({
12+
required String adTagUrl,
13+
ContentProgressProvider? contentProgressProvider,
14+
}) : this.fromPlatform(
15+
PlatformAdsRequest(
16+
adTagUrl: adTagUrl,
17+
contentProgressProvider: contentProgressProvider?.platform,
18+
),
19+
);
20+
21+
/// Constructs an [AdsRequest] from a specific platform implementation.
22+
AdsRequest.fromPlatform(this.platform);
23+
24+
/// Implementation of [PlatformAdsRequest] for the current platform.
25+
final PlatformAdsRequest platform;
26+
27+
/// The URL from which ads will be requested.
28+
String get adTagUrl => platform.adTagUrl;
29+
30+
/// A [ContentProgressProvider] instance to allow scheduling of ad breaks
31+
/// based on content progress (cue points).
32+
ContentProgressProvider? get contentProgressProvider => platform
33+
.contentProgressProvider !=
34+
null
35+
? ContentProgressProvider.fromPlatform(platform.contentProgressProvider!)
36+
: null;
37+
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:flutter/widgets.dart';
99
import '../platform_interface/platform_interface.dart';
1010
import 'android_ad_display_container.dart';
1111
import 'android_ads_manager.dart';
12+
import 'android_content_progress_provider.dart';
1213
import 'enum_converter_utils.dart';
1314
import 'interactive_media_ads.g.dart' as ima;
1415
import 'interactive_media_ads_proxy.dart';
@@ -79,13 +80,18 @@ base class AndroidAdsLoader extends PlatformAdsLoader {
7980
}
8081

8182
@override
82-
Future<void> requestAds(AdsRequest request) async {
83+
Future<void> requestAds(PlatformAdsRequest request) async {
8384
final ima.AdsLoader adsLoader = await _adsLoaderFuture;
8485

8586
final ima.AdsRequest androidRequest = await _sdkFactory.createAdsRequest();
8687

8788
await Future.wait(<Future<void>>[
8889
androidRequest.setAdTagUrl(request.adTagUrl),
90+
if (request.contentProgressProvider != null)
91+
androidRequest.setContentProgressProvider(
92+
(request.contentProgressProvider! as AndroidContentProgressProvider)
93+
.progressProvider,
94+
),
8995
adsLoader.requestAds(androidRequest),
9096
]);
9197
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
7+
import 'package:meta/meta.dart';
8+
9+
import '../platform_interface/platform_content_progress_provider.dart';
10+
import 'interactive_media_ads.g.dart' as ima;
11+
import 'interactive_media_ads_proxy.dart';
12+
13+
/// Android implementation of [PlatformContentProgressProviderCreationParams].
14+
final class AndroidContentProgressProviderCreationParams
15+
extends PlatformContentProgressProviderCreationParams {
16+
/// Constructs a [AndroidContentProgressProviderCreationParams].
17+
const AndroidContentProgressProviderCreationParams({
18+
@visibleForTesting InteractiveMediaAdsProxy? proxy,
19+
}) : _proxy = proxy ?? const InteractiveMediaAdsProxy(),
20+
super();
21+
22+
/// Creates a [AndroidContentProgressProviderCreationParams] from an instance of
23+
/// [PlatformContentProgressProviderCreationParams].
24+
factory AndroidContentProgressProviderCreationParams.fromPlatformContentProgressProviderCreationParams(
25+
// Placeholder to prevent requiring a breaking change if params are added to
26+
// PlatformContentProgressProviderCreationParams.
27+
// ignore: avoid_unused_constructor_parameters
28+
PlatformContentProgressProviderCreationParams params, {
29+
@visibleForTesting InteractiveMediaAdsProxy? proxy,
30+
}) {
31+
return AndroidContentProgressProviderCreationParams(proxy: proxy);
32+
}
33+
34+
final InteractiveMediaAdsProxy _proxy;
35+
}
36+
37+
/// Android implementation of [PlatformContentProgressProvider].
38+
base class AndroidContentProgressProvider
39+
extends PlatformContentProgressProvider {
40+
/// Constructs an [AndroidContentProgressProvider].
41+
AndroidContentProgressProvider(super.params) : super.implementation();
42+
43+
/// The native Android ContentProgressProvider.
44+
///
45+
/// This allows the SDK to track progress of the content video.
46+
@internal
47+
late final ima.ContentProgressProvider progressProvider =
48+
_androidParams._proxy.newContentProgressProvider();
49+
50+
late final AndroidContentProgressProviderCreationParams _androidParams =
51+
params is AndroidContentProgressProviderCreationParams
52+
? params as AndroidContentProgressProviderCreationParams
53+
: AndroidContentProgressProviderCreationParams
54+
.fromPlatformContentProgressProviderCreationParams(
55+
params,
56+
);
57+
58+
@override
59+
Future<void> setProgress({
60+
required Duration progress,
61+
required Duration duration,
62+
}) async {
63+
return progressProvider.setContentProgress(
64+
_androidParams._proxy.newVideoProgressUpdate(
65+
currentTimeMs: progress.inMilliseconds,
66+
durationMs: duration.inMilliseconds,
67+
),
68+
);
69+
}
70+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import '../platform_interface/interactive_media_ads_platform.dart';
66
import '../platform_interface/platform_ad_display_container.dart';
77
import '../platform_interface/platform_ads_loader.dart';
88
import '../platform_interface/platform_ads_manager_delegate.dart';
9+
import '../platform_interface/platform_content_progress_provider.dart';
910
import 'android_ad_display_container.dart';
1011
import 'android_ads_loader.dart';
1112
import 'android_ads_manager_delegate.dart';
13+
import 'android_content_progress_provider.dart';
1214

1315
/// Android implementation of [InteractiveMediaAdsPlatform].
1416
final class AndroidInteractiveMediaAds extends InteractiveMediaAdsPlatform {
@@ -37,4 +39,11 @@ final class AndroidInteractiveMediaAds extends InteractiveMediaAdsPlatform {
3739
) {
3840
return AndroidAdsManagerDelegate(params);
3941
}
42+
43+
@override
44+
PlatformContentProgressProvider createPlatformContentProgressProvider(
45+
PlatformContentProgressProviderCreationParams params,
46+
) {
47+
return AndroidContentProgressProvider(params);
48+
}
4049
}

0 commit comments

Comments
 (0)