Skip to content

[interactive_media_ads] Adds support for pausing and resuming Ad playback and skipping an Ad #7285

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

Merged
merged 26 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
56d350c
android impl of the other adsmanager methods
bparrishMines Jul 31, 2024
a5d5974
ios impl
bparrishMines Jul 31, 2024
6dc5253
update tests
bparrishMines Jul 31, 2024
cbd1997
android implementationc
bparrishMines Jul 31, 2024
cc30861
docs and some moves
bparrishMines Jul 31, 2024
fea4ad5
version bump
bparrishMines Jul 31, 2024
c322df0
test for android change
bparrishMines Aug 1, 2024
65029c9
Merge branch 'main' of github.com:flutter/packages into ima_adsmanager
bparrishMines Aug 1, 2024
cd90b6c
update excerpts
bparrishMines Aug 1, 2024
db84d1f
Merge branch 'main' of github.com:flutter/packages into ima_adsmanager
bparrishMines Aug 1, 2024
12963c7
kotlin lint and pigeon remove
bparrishMines Aug 2, 2024
0287cce
Merge branch 'main' of github.com:flutter/packages into ima_adsmanager
bparrishMines Aug 9, 2024
c419c4f
Merge branch 'main' of github.com:flutter/packages into ima_adsmanager
bparrishMines Aug 20, 2024
e301fbb
update to other pr
bparrishMines Aug 20, 2024
ba4bbd0
fix bug
bparrishMines Aug 21, 2024
81add1b
update excerpts
bparrishMines Aug 21, 2024
868e745
Merge branch 'main' of github.com:flutter/packages into ima_adsmanager
bparrishMines Aug 21, 2024
aeac8ff
remove reference to app lifecycle tate
bparrishMines Aug 21, 2024
6f53e2c
Merge branch 'main' of github.com:flutter/packages into ima_adsmanager
bparrishMines Aug 29, 2024
45a8fca
Merge branch 'main' of github.com:flutter/packages into ima_adsmanager
bparrishMines Aug 29, 2024
6d7ef88
update generation
bparrishMines Aug 29, 2024
6920e62
change var name and ish
bparrishMines Aug 29, 2024
363fdbc
remove fix and reset state
bparrishMines Aug 29, 2024
224ba02
small comment
bparrishMines Aug 29, 2024
1b09458
Merge branch 'main' of github.com:flutter/packages into ima_adsmanager
bparrishMines Aug 30, 2024
42cfd63
unchange files
bparrishMines Aug 30, 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
7 changes: 7 additions & 0 deletions packages/interactive_media_ads/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 0.2.0

* Adds support for pausing and resuming Ad playback. See `AdsManager.pause` and `AdsManager.resume`.
* Adds support to skip an Ad. See `AdsManager.skip` and `AdsManager.discardAdBreak`.
* **Breaking Change** To keep platform consistency, Android no longer continues playing an Ad
whenever it returns from an Ad click. Call `AdsManager.resume` to resume Ad playback.

## 0.1.2+6

* Fixes bug where the ad would play when the app returned to foreground during content playback.
Expand Down
12 changes: 4 additions & 8 deletions packages/interactive_media_ads/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ platform classes that are returned by this.
#### SDK Wrappers

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

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

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

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

Android:

```yaml
pigeon:
git:
url: git@github.com:bparrishMines/packages.git
ref: pigeon_kotlin_split
path: packages/pigeon
pigeon: ^22.2.0
```

iOS:
Expand Down
10 changes: 7 additions & 3 deletions packages/interactive_media_ads/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ class AdExampleWidget extends StatefulWidget {
State<AdExampleWidget> createState() => _AdExampleWidgetState();
}

class _AdExampleWidgetState extends State<AdExampleWidget> {
class _AdExampleWidgetState extends State<AdExampleWidget>
with WidgetsBindingObserver {
// 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 =
Expand All @@ -91,6 +92,7 @@ class _AdExampleWidgetState extends State<AdExampleWidget> {
// 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;
Expand Down Expand Up @@ -124,6 +126,7 @@ late final AdDisplayContainer _adDisplayContainer = AdDisplayContainer(
@override
void initState() {
super.initState();
// ···
_contentVideoController = VideoPlayerController.networkUrl(
Uri.parse(
'https://storage.googleapis.com/gvabox/media/samples/stock.mp4',
Expand All @@ -132,8 +135,8 @@ void initState() {
..addListener(() {
if (_contentVideoController.value.isCompleted) {
_adsLoader.contentComplete();
setState(() {});
}
setState(() {});
})
..initialize().then((_) {
// Ensure the first frame is shown after the video is initialized, even before the play button has been pressed.
Expand Down Expand Up @@ -257,7 +260,7 @@ Future<void> _pauseContent() {

### 7. Dispose Resources

Dispose the content player and the destroy the [AdsManager][6].
Dispose the content player and destroy the [AdsManager][6].

<?code-excerpt "example/lib/main.dart (dispose)"?>
```dart
Expand All @@ -266,6 +269,7 @@ void dispose() {
super.dispose();
_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.1.2+6"
const val pluginVersion = "0.2.0"
}

override fun setAdTagUrl(pigeon_instance: AdsRequest, adTagUrl: String) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// 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.
// Autogenerated from Pigeon (v22.1.0), do not edit directly.
// Autogenerated from Pigeon (v22.2.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass", "SyntheticAccessor")

Expand Down
41 changes: 39 additions & 2 deletions packages/interactive_media_ads/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ class AdExampleWidget extends StatefulWidget {
State<AdExampleWidget> createState() => _AdExampleWidgetState();
}

class _AdExampleWidgetState extends State<AdExampleWidget> {
class _AdExampleWidgetState extends State<AdExampleWidget>
with WidgetsBindingObserver {
// 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 =
Expand All @@ -44,6 +45,11 @@ class _AdExampleWidgetState extends State<AdExampleWidget> {
// AdsManager exposes methods to control ad playback and listen to ad events.
AdsManager? _adsManager;

// #enddocregion example_widget
// Last state received in `didChangeAppLifecycleState`.
AppLifecycleState _lastLifecycleState = AppLifecycleState.resumed;

// #docregion example_widget
// Whether the widget should be displaying the content video. The content
// player is hidden while Ads are playing.
bool _shouldShowContentVideo = true;
Expand All @@ -64,6 +70,11 @@ class _AdExampleWidgetState extends State<AdExampleWidget> {
@override
void initState() {
super.initState();
// #enddocregion ad_and_content_players
// Adds this instance as an observer for `AppLifecycleState` changes.
WidgetsBinding.instance.addObserver(this);

// #docregion ad_and_content_players
_contentVideoController = VideoPlayerController.networkUrl(
Uri.parse(
'https://storage.googleapis.com/gvabox/media/samples/stock.mp4',
Expand All @@ -72,8 +83,8 @@ class _AdExampleWidgetState extends State<AdExampleWidget> {
..addListener(() {
if (_contentVideoController.value.isCompleted) {
_adsLoader.contentComplete();
setState(() {});
}
setState(() {});
})
..initialize().then((_) {
// Ensure the first frame is shown after the video is initialized, even before the play button has been pressed.
Expand All @@ -82,6 +93,29 @@ class _AdExampleWidgetState extends State<AdExampleWidget> {
}
// #enddocregion ad_and_content_players

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
if (!_shouldShowContentVideo) {
_adsManager?.resume();
}
case AppLifecycleState.inactive:
// Pausing the Ad video player on Android can only be done in this state
// because it corresponds to `Activity.onPause`. This state is also
// triggered before resume, so this will only pause the Ad if the app is
// in the process of being sent to the background.
if (!_shouldShowContentVideo &&
_lastLifecycleState == AppLifecycleState.resumed) {
_adsManager?.pause();
}
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
}
_lastLifecycleState = state;
}

// #docregion request_ads
Future<void> _requestAds(AdDisplayContainer container) {
_adsLoader = AdsLoader(
Expand Down Expand Up @@ -146,6 +180,9 @@ class _AdExampleWidgetState extends State<AdExampleWidget> {
super.dispose();
_contentVideoController.dispose();
_adsManager?.destroy();
// #enddocregion dispose
WidgetsBinding.instance.removeObserver(this);
// #docregion dispose
}
// #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.1.2+6"
static let pluginVersion = "0.2.0"

func pigeonDefaultConstructor(
pigeonApi: PigeonApiIMAAdsRequest, adTagUrl: String, adDisplayContainer: IMAAdDisplayContainer,
Expand Down
24 changes: 24 additions & 0 deletions packages/interactive_media_ads/lib/src/ads_loader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,30 @@ class AdsManager {
return platform.setAdsManagerDelegate(delegate.platform);
}

/// Pauses the current ad.
Future<void> pause() {
return platform.pause();
}

/// Resumes the current ad.
Future<void> resume() {
return platform.resume();
}

/// Skips the current ad.
///
/// This only skips ads if IMA does not render the 'Skip ad' button.
Future<void> skip() {
return platform.skip();
}

/// Discards current ad break and resumes content.
///
/// If there is no current ad then the next ad break is discarded.
Future<void> discardAdBreak() {
return platform.discardAdBreak();
}

/// Stops the ad and all tracking, then releases all assets that were loaded
/// to play the ad.
Future<void> destroy() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ base class AndroidAdDisplayContainer extends PlatformAdDisplayContainer {

int? _adDuration;

// Whether MediaPlayer.start() should be called whenever the VideoView
// `onPrepared` callback is triggered. `onPrepared` is triggered whenever the
// app is resumed after being inactive.
bool _startPlayerWhenVideoIsPrepared = true;

late final AndroidAdDisplayContainerCreationParams _androidParams =
params is AndroidAdDisplayContainerCreationParams
? params as AndroidAdDisplayContainerCreationParams
Expand Down Expand Up @@ -217,10 +222,12 @@ base class AndroidAdDisplayContainer extends PlatformAdDisplayContainer {
if (container._savedAdPosition > 0) {
await player.seekTo(container._savedAdPosition);
}
}

await player.start();
container?._startAdProgressTracking();
if (container._startPlayerWhenVideoIsPrepared) {
await player.start();
container._startAdProgressTracking();
}
}
},
onError: (_, __, ___, ____) {
final AndroidAdDisplayContainer? container = weakThis.target;
Expand Down Expand Up @@ -256,24 +263,33 @@ base class AndroidAdDisplayContainer extends PlatformAdDisplayContainer {
pauseAd: (_, __) async {
final AndroidAdDisplayContainer? container = weakThis.target;
if (container != null) {
// Setting this to false ensures the ad doesn't start playing if an
// app is returned to the foreground.
container._startPlayerWhenVideoIsPrepared = false;
await container._mediaPlayer!.pause();
container._savedAdPosition =
await container._videoView.getCurrentPosition();
container._stopAdProgressTracking();
}
},
playAd: (_, ima.AdMediaInfo adMediaInfo) {
weakThis.target?._videoView.setVideoUri(adMediaInfo.url);
final AndroidAdDisplayContainer? container = weakThis.target;
if (container != null) {
container._startPlayerWhenVideoIsPrepared = true;
container._videoView.setVideoUri(adMediaInfo.url);
}
},
release: (_) {},
stopAd: (_, __) {
final AndroidAdDisplayContainer? container = weakThis.target;
if (container != null) {
// Clear and reset all state.
container._stopAdProgressTracking();
container._videoView.setVideoUri(null);
container._clearMediaPlayer();
container._loadedAdMediaInfo = null;
container._adDuration = null;
container._startPlayerWhenVideoIsPrepared = true;
}
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,26 @@ class AndroidAdsManager extends PlatformAdsManager {
return _manager.start();
}

@override
Future<void> discardAdBreak() {
return _manager.discardAdBreak();
}

@override
Future<void> pause() {
return _manager.pause();
}

@override
Future<void> resume() {
return _manager.resume();
}

@override
Future<void> skip() {
return _manager.skip();
}

// This value is created in a static method because the callback methods for
// any wrapped classes must not reference the encapsulating object. This is to
// prevent a circular reference that prevents garbage collection.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// 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.
// Autogenerated from Pigeon (v22.1.0), do not edit directly.
// Autogenerated from Pigeon (v22.2.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers

Expand Down
20 changes: 20 additions & 0 deletions packages/interactive_media_ads/lib/src/ios/ios_ads_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,24 @@ class IOSAdsManager extends PlatformAdsManager {
Future<void> start(AdsManagerStartParams params) {
return _manager.start();
}

@override
Future<void> discardAdBreak() {
return _manager.discardAdBreak();
}

@override
Future<void> pause() {
return _manager.pause();
}

@override
Future<void> resume() {
return _manager.resume();
}

@override
Future<void> skip() {
return _manager.skip();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ abstract class PlatformAdsManager {
/// /// The [AdsManagerDelegate] to notify with events during ad playback.
Future<void> setAdsManagerDelegate(PlatformAdsManagerDelegate delegate);

/// Pauses the current ad.
Future<void> pause();

/// Resumes the current ad.
Future<void> resume();

/// Skips the current ad.
///
/// This only skips ads if IMA does not render the 'Skip ad' button.
Future<void> skip();

/// Discards current ad break and resumes content.
///
/// If there is no current ad then the next ad break is discarded.
Future<void> discardAdBreak();

/// Stops the ad and all tracking, then releases all assets that were loaded
/// to play the ad.
Future<void> destroy();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// https://github.com/flutter/packages/pull/6371 lands. This file uses the
// Kotlin ProxyApi feature from pigeon.
// ignore_for_file: avoid_unused_constructor_parameters
/*

import 'package:pigeon/pigeon.dart';

@ConfigurePigeon(
Expand Down Expand Up @@ -738,4 +738,3 @@ abstract class AdEventListener {
/// Respond to an occurrence of an AdEvent.
late final void Function(AdEvent event) onAdEvent;
}
*/
Loading