Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[interactive_media_ads] Adds support for mid-roll ads #7407

Merged
merged 32 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
905cd48
interface and app-facing implementation for content progress provider
bparrishMines Aug 6, 2024
89e4434
partial android implementation
bparrishMines Aug 6, 2024
427ded9
pass progress and duration to set progress
bparrishMines Aug 6, 2024
e8fd08d
android implementation mostly
bparrishMines Aug 6, 2024
55e080b
separate adsrequest into its own app-facing class
bparrishMines Aug 6, 2024
76efd5a
finish android impl and add to example app
bparrishMines Aug 6, 2024
83ac065
working mid rolls
bparrishMines Aug 7, 2024
837e345
fix test names
bparrishMines Aug 7, 2024
fb2baee
android tests
bparrishMines Aug 7, 2024
cef317c
change to swift gen
bparrishMines Aug 7, 2024
213f2da
ios impl
bparrishMines Aug 8, 2024
209c544
android class
bparrishMines Aug 21, 2024
ce5a8be
version bump
bparrishMines Aug 21, 2024
8d61008
comment pigeon android again
bparrishMines Aug 21, 2024
0e5e86c
Merge branch 'main' of github.com:flutter/packages into ima_contentpp
bparrishMines Sep 10, 2024
bc9c047
add the suppress
bparrishMines Sep 10, 2024
3c56859
comment out content duration
bparrishMines Sep 10, 2024
e5d1ba6
Merge branch 'main' of github.com:flutter/packages into ima_midrolls
bparrishMines Sep 10, 2024
90181e6
regen and format
bparrishMines Sep 10, 2024
a799357
fix version bump
bparrishMines Sep 10, 2024
86ddf15
Merge branch 'ima_contentpp' into ima_midrolls
bparrishMines Sep 10, 2024
4f2e9ff
update geng
bparrishMines Sep 10, 2024
9e883fe
update pr to other pr
bparrishMines Sep 10, 2024
f1e6897
try to test requestads
bparrishMines Sep 10, 2024
8804a14
Merge branch 'main' of github.com:flutter/packages into ima_midrolls
bparrishMines Sep 11, 2024
a5d73a8
version bump
bparrishMines Sep 11, 2024
e458f49
readme update
bparrishMines Sep 11, 2024
5fb899b
update i think
bparrishMines Sep 11, 2024
a702ffd
Merge branch 'main' of github.com:flutter/packages into ima_midrolls
bparrishMines Sep 11, 2024
c80ef68
improve imports
bparrishMines Sep 11, 2024
d183264
update interval
bparrishMines Sep 17, 2024
2319286
Merge branch 'main' of github.com:flutter/packages into ima_midrolls
bparrishMines Sep 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: 500),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@harold1208 @stuartmorgan What are your thoughts on the interval to be used here in the example? This works for me on Android and iOS, but this is a Flutter specific workaround to handle the SDK running on a separate thread.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For context, this timer updates the SDK of the progress of the content video with this being the interval between updates.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per our offline discussion with the IMA team, a 200ms interval would be the ideal value.

(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: 500),
(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
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_ads_request.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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The platform implementations haven't been exposed yet, so this shouldn't be a breaking change.

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