Skip to content

Commit

Permalink
[interactive_media_ads] Adds support for mid-roll ads (#7407)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bparrishMines committed Sep 17, 2024
1 parent 4f2b9cd commit c7406b3
Show file tree
Hide file tree
Showing 37 changed files with 910 additions and 44 deletions.
4 changes: 4 additions & 0 deletions packages/interactive_media_ads/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.2.2

* Adds support for mid-roll ads. See `AdsRequest.contentProgressProvider`.

## 0.2.1

* Adds internal wrapper for Android native `ContentProgressProvider`.
Expand Down
43 changes: 38 additions & 5 deletions packages/interactive_media_ads/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ class AdExampleWidget extends StatefulWidget {
class _AdExampleWidgetState extends State<AdExampleWidget>
with WidgetsBindingObserver {
// IMA sample tag for a single skippable inline video ad. See more IMA sample
// IMA sample tag for a pre-, mid-, and post-roll, single 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=';
'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=';
// The AdsLoader instance exposes the request ads method.
late final AdsLoader _adsLoader;
Expand All @@ -99,6 +99,15 @@ class _AdExampleWidgetState extends State<AdExampleWidget>
// Controls the content video player.
late final VideoPlayerController _contentVideoController;
// Periodically updates the SDK of the current playback progress of the
// content video.
Timer? _contentProgressTimer;
// Provides the SDK with the current playback progress of the content video.
// This is required to support mid-roll ads.
final ContentProgressProvider _contentProgressProvider =
ContentProgressProvider();
// ···
@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -240,20 +249,43 @@ Future<void> _requestAds(AdDisplayContainer container) {
},
);
return _adsLoader.requestAds(AdsRequest(adTagUrl: _adTagUrl));
return _adsLoader.requestAds(AdsRequest(
adTagUrl: _adTagUrl,
contentProgressProvider: _contentProgressProvider,
));
}
Future<void> _resumeContent() {
Future<void> _resumeContent() async {
setState(() {
_shouldShowContentVideo = true;
});
return _contentVideoController.play();
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();
}
```
Expand All @@ -267,6 +299,7 @@ Dispose the content player and destroy the [AdsManager][6].
@override
void dispose() {
super.dispose();
_contentProgressTimer?.cancel();
_contentVideoController.dispose();
_adsManager?.destroy();
// ···
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class AdsRequestProxyApi(override val pigeonRegistrar: ProxyApiRegistrar) :
*
* This must match the version in pubspec.yaml.
*/
const val pluginVersion = "0.2.1"
const val pluginVersion = "0.2.2"
}

override fun setAdTagUrl(pigeon_instance: AdsRequest, adTagUrl: String) {
Expand Down
43 changes: 38 additions & 5 deletions packages/interactive_media_ads/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ class AdExampleWidget extends StatefulWidget {

class _AdExampleWidgetState extends State<AdExampleWidget>
with WidgetsBindingObserver {
// IMA sample tag for a single skippable inline video ad. See more IMA sample
// IMA sample tag for a pre-, mid-, and post-roll, single 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=';
'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=';

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

// Controls the content video player.
late final VideoPlayerController _contentVideoController;

// Periodically updates the SDK of the current playback progress of the
// content video.
Timer? _contentProgressTimer;

// Provides the SDK with the current playback progress of the content video.
// This is required to support mid-roll ads.
final ContentProgressProvider _contentProgressProvider =
ContentProgressProvider();
// #enddocregion example_widget

// #docregion ad_and_content_players
Expand Down Expand Up @@ -156,20 +165,43 @@ class _AdExampleWidgetState extends State<AdExampleWidget>
},
);

return _adsLoader.requestAds(AdsRequest(adTagUrl: _adTagUrl));
return _adsLoader.requestAds(AdsRequest(
adTagUrl: _adTagUrl,
contentProgressProvider: _contentProgressProvider,
));
}

Future<void> _resumeContent() {
Future<void> _resumeContent() async {
setState(() {
_shouldShowContentVideo = true;
});
return _contentVideoController.play();

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();
}
// #enddocregion request_ads
Expand All @@ -178,6 +210,7 @@ class _AdExampleWidgetState extends State<AdExampleWidget>
@override
void dispose() {
super.dispose();
_contentProgressTimer?.cancel();
_contentVideoController.dispose();
_adsManager?.destroy();
// #enddocregion dispose
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class AdsRequestProxyAPIDelegate: PigeonApiDelegateIMAAdsRequest {
/// The current version of the `interactive_media_ads` plugin.
///
/// This must match the version in pubspec.yaml.
static let pluginVersion = "0.2.1"
static let pluginVersion = "0.2.2"

func pigeonDefaultConstructor(
pigeonApi: PigeonApiIMAAdsRequest, adTagUrl: String, adDisplayContainer: IMAAdDisplayContainer,
Expand Down
5 changes: 3 additions & 2 deletions packages/interactive_media_ads/lib/interactive_media_ads.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
export 'src/ad_display_container.dart';
export 'src/ads_loader.dart';
export 'src/ads_manager_delegate.dart';
export 'src/ads_request.dart';
export 'src/android/android_interactive_media_ads.dart'
show AndroidInteractiveMediaAds;
export 'src/content_progress_provider.dart';
export 'src/ios/ios_interactive_media_ads.dart' show IOSInteractiveMediaAds;
export 'src/platform_interface/platform_interface.dart'
show
Expand All @@ -16,5 +18,4 @@ export 'src/platform_interface/platform_interface.dart'
AdErrorType,
AdEvent,
AdEventType,
AdsLoadErrorData,
AdsRequest;
AdsLoadErrorData;
3 changes: 2 additions & 1 deletion packages/interactive_media_ads/lib/src/ads_loader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';

import 'ad_display_container.dart';
import 'ads_manager_delegate.dart';
import 'ads_request.dart';
import 'platform_interface/platform_interface.dart';

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

Expand Down
37 changes: 37 additions & 0 deletions packages/interactive_media_ads/lib/src/ads_request.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'content_progress_provider.dart';
import 'platform_interface/platform_interface.dart';

/// An object containing the data used to request ads from the server.
class AdsRequest {
/// Creates an [AdsRequest].
AdsRequest({
required String adTagUrl,
ContentProgressProvider? contentProgressProvider,
}) : this.fromPlatform(
PlatformAdsRequest(
adTagUrl: adTagUrl,
contentProgressProvider: contentProgressProvider?.platform,
),
);

/// Constructs an [AdsRequest] from a specific platform implementation.
AdsRequest.fromPlatform(this.platform);

/// Implementation of [PlatformAdsRequest] for the current platform.
final PlatformAdsRequest platform;

/// The URL from which ads will be requested.
String get adTagUrl => platform.adTagUrl;

/// A [ContentProgressProvider] instance to allow scheduling of ad breaks
/// based on content progress (cue points).
ContentProgressProvider? get contentProgressProvider => platform
.contentProgressProvider !=
null
? ContentProgressProvider.fromPlatform(platform.contentProgressProvider!)
: null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:flutter/widgets.dart';
import '../platform_interface/platform_interface.dart';
import 'android_ad_display_container.dart';
import 'android_ads_manager.dart';
import 'android_content_progress_provider.dart';
import 'enum_converter_utils.dart';
import 'interactive_media_ads.g.dart' as ima;
import 'interactive_media_ads_proxy.dart';
Expand Down Expand Up @@ -79,13 +80,18 @@ base class AndroidAdsLoader extends PlatformAdsLoader {
}

@override
Future<void> requestAds(AdsRequest request) async {
Future<void> requestAds(PlatformAdsRequest request) async {
final ima.AdsLoader adsLoader = await _adsLoaderFuture;

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

await Future.wait(<Future<void>>[
androidRequest.setAdTagUrl(request.adTagUrl),
if (request.contentProgressProvider != null)
androidRequest.setContentProgressProvider(
(request.contentProgressProvider! as AndroidContentProgressProvider)
.progressProvider,
),
adsLoader.requestAds(androidRequest),
]);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:meta/meta.dart';

import '../platform_interface/platform_content_progress_provider.dart';
import 'interactive_media_ads.g.dart' as ima;
import 'interactive_media_ads_proxy.dart';

/// Android implementation of [PlatformContentProgressProviderCreationParams].
final class AndroidContentProgressProviderCreationParams
extends PlatformContentProgressProviderCreationParams {
/// Constructs a [AndroidContentProgressProviderCreationParams].
const AndroidContentProgressProviderCreationParams({
@visibleForTesting InteractiveMediaAdsProxy? proxy,
}) : _proxy = proxy ?? const InteractiveMediaAdsProxy(),
super();

/// Creates a [AndroidContentProgressProviderCreationParams] from an instance of
/// [PlatformContentProgressProviderCreationParams].
factory AndroidContentProgressProviderCreationParams.fromPlatformContentProgressProviderCreationParams(
// Placeholder to prevent requiring a breaking change if params are added to
// PlatformContentProgressProviderCreationParams.
// ignore: avoid_unused_constructor_parameters
PlatformContentProgressProviderCreationParams params, {
@visibleForTesting InteractiveMediaAdsProxy? proxy,
}) {
return AndroidContentProgressProviderCreationParams(proxy: proxy);
}

final InteractiveMediaAdsProxy _proxy;
}

/// Android implementation of [PlatformContentProgressProvider].
base class AndroidContentProgressProvider
extends PlatformContentProgressProvider {
/// Constructs an [AndroidContentProgressProvider].
AndroidContentProgressProvider(super.params) : super.implementation();

/// The native Android ContentProgressProvider.
///
/// This allows the SDK to track progress of the content video.
@internal
late final ima.ContentProgressProvider progressProvider =
_androidParams._proxy.newContentProgressProvider();

late final AndroidContentProgressProviderCreationParams _androidParams =
params is AndroidContentProgressProviderCreationParams
? params as AndroidContentProgressProviderCreationParams
: AndroidContentProgressProviderCreationParams
.fromPlatformContentProgressProviderCreationParams(
params,
);

@override
Future<void> setProgress({
required Duration progress,
required Duration duration,
}) async {
return progressProvider.setContentProgress(
_androidParams._proxy.newVideoProgressUpdate(
currentTimeMs: progress.inMilliseconds,
durationMs: duration.inMilliseconds,
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import '../platform_interface/interactive_media_ads_platform.dart';
import '../platform_interface/platform_ad_display_container.dart';
import '../platform_interface/platform_ads_loader.dart';
import '../platform_interface/platform_ads_manager_delegate.dart';
import '../platform_interface/platform_content_progress_provider.dart';
import 'android_ad_display_container.dart';
import 'android_ads_loader.dart';
import 'android_ads_manager_delegate.dart';
import 'android_content_progress_provider.dart';

/// Android implementation of [InteractiveMediaAdsPlatform].
final class AndroidInteractiveMediaAds extends InteractiveMediaAdsPlatform {
Expand Down Expand Up @@ -37,4 +39,11 @@ final class AndroidInteractiveMediaAds extends InteractiveMediaAdsPlatform {
) {
return AndroidAdsManagerDelegate(params);
}

@override
PlatformContentProgressProvider createPlatformContentProgressProvider(
PlatformContentProgressProviderCreationParams params,
) {
return AndroidContentProgressProvider(params);
}
}
Loading

0 comments on commit c7406b3

Please sign in to comment.