npaw_forge_interactive_media_ads 10.2.0
npaw_forge_interactive_media_ads: ^10.2.0 copied to clipboard
NPAW Forge ad adapter for Google's interactive_media_ads Flutter plugin: reports typed IMA ad break, quartile and error facts to the Forge core.
example/lib/main.dart
// Copyright (C) NPAW - All Rights Reserved
//
// This source code is protected under international copyright law. All rights
// reserved and protected by the copyright holders. This file is confidential and
// only available to authorized individuals with the permission of the copyright
// holders. If you encounter this file and do not have permission, please contact
// the copyright holders and delete this file.
//
//
// @author: Victor Soto <victor.soto@nicepeopleatwork.com>
// @description: NPAW Forge IMA example playing a video_player stream with a sample VAST pre-roll and full analytics.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:interactive_media_ads/interactive_media_ads.dart';
import 'package:npaw_forge/npaw_forge.dart';
import 'package:npaw_forge_interactive_media_ads/npaw_forge_interactive_media_ads.dart';
import 'package:npaw_forge_video_player/npaw_forge_video_player.dart';
import 'package:video_player/video_player.dart';
/// Demo account; override with --dart-define=FORGE_ACCOUNT_CODE=yourAccount.
const String accountCode = String.fromEnvironment(
'FORGE_ACCOUNT_CODE',
defaultValue: 'npawdemodev',
);
/// Ingest endpoint; point smoke runs at stage, never production.
const String lmaEndpoint = String.fromEnvironment('FORGE_LMA_ENDPOINT');
const String sampleStream = String.fromEnvironment(
'FORGE_SAMPLE_STREAM',
defaultValue: 'https://storage.googleapis.com/gvabox/media/samples/stock.mp4',
);
/// Google's public single-linear sample VAST tag.
const String sampleAdTag = String.fromEnvironment(
'FORGE_AD_TAG',
defaultValue:
'https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/'
'single_ad_samples&sz=640x480&cust_params=sample_ct%3Dlinear&'
'ciu_szs=300x250%2C728x90&gdfp_req=1&output=vast&'
'unviewed_position_start=1&env=vp&impl=s&correlator=',
);
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final forge = await NpawForge.start(
ForgeConfig(
accountCode: accountCode,
appName: 'npaw-forge-ima-example',
lmaEndpoint: lmaEndpoint.isEmpty ? null : lmaEndpoint,
logLevel: 'info',
),
);
const debugDiagnostics =
bool.fromEnvironment('FORGE_DEBUG_DIAGNOSTICS', defaultValue: false);
if (debugDiagnostics) {
forge.events.listen((event) => debugPrint('forge event: ${event.raw}'));
}
WidgetsBinding.instance.addObserver(ForgeLifecycleObserver(forge));
runApp(ExampleApp(forge: forge));
}
class ExampleApp extends StatefulWidget {
const ExampleApp({required this.forge, super.key});
final NpawForge forge;
@override
State<ExampleApp> createState() => _ExampleAppState();
}
class _ExampleAppState extends State<ExampleApp> {
late final VideoPlayerController _contentController =
VideoPlayerController.networkUrl(Uri.parse(sampleStream));
/// Content-side Forge binding; its adapter also couples pause/resume
/// around ad breaks through the IMA attachment below.
late final Future<ForgeVideoPlayerAttachment> _contentAttachmentFuture;
late final AdsLoader _adsLoader;
AdsManager? _adsManager;
ForgeImaAdsAttachment? _adsAttachment;
bool _shouldShowContentVideo = false;
Timer? _contentProgressTimer;
final ContentProgressProvider _contentProgressProvider =
ContentProgressProvider();
late final AdDisplayContainer _adDisplayContainer = AdDisplayContainer(
onContainerAdded: (AdDisplayContainer container) {
_adsLoader = AdsLoader(
container: container,
onAdsLoaded: _onAdsLoaded,
onAdsLoadError: (AdsLoadErrorData data) {
debugPrint('OnAdsLoadError: ${data.error.message}');
unawaited(_resumeContent());
},
);
// Ads can only be requested once the container is in the native
// view hierarchy.
unawaited(
_adsLoader.requestAds(
AdsRequest(
adTagUrl: sampleAdTag,
contentProgressProvider: _contentProgressProvider,
),
),
);
},
);
@override
void initState() {
super.initState();
_contentAttachmentFuture = attachVideoPlayer(
widget.forge,
_contentController,
metadata: const ContentMetadata(title: 'IMA sample stream'),
);
_contentController
..addListener(() {
if (_contentController.value.isCompleted) {
unawaited(_adsLoader.contentComplete());
}
setState(() {});
})
..initialize().then((_) => setState(() {}));
}
Future<void> _onAdsLoaded(OnAdsLoadedData data) async {
final manager = data.manager;
_adsManager = manager;
final contentAttachment = await _contentAttachmentFuture;
// Single delegate per manager: Forge maps events first, then the app
// callbacks below keep driving playback.
_adsAttachment = await attachImaAds(
contentAttachment.video,
manager,
contentAdapter: contentAttachment.adapter,
onAdEvent: (AdEvent event) {
debugPrint('OnAdEvent: ${event.type}');
switch (event.type) {
case AdEventType.loaded:
unawaited(manager.start());
case AdEventType.contentPauseRequested:
unawaited(_pauseContent());
case AdEventType.contentResumeRequested:
unawaited(_resumeContent());
case AdEventType.allAdsCompleted:
unawaited(manager.destroy());
_adsManager = null;
default:
break;
}
},
onAdErrorEvent: (AdErrorEvent event) {
debugPrint('AdErrorEvent: ${event.error.message}');
unawaited(_resumeContent());
},
);
await manager.init();
}
Future<void> _resumeContent() async {
if (!mounted) {
return;
}
setState(() {
_shouldShowContentVideo = true;
});
if (_adsManager != null) {
_contentProgressTimer = Timer.periodic(
const Duration(milliseconds: 200),
(Timer timer) async {
if (_contentController.value.isInitialized) {
final progress = await _contentController.position;
if (progress != null) {
await _contentProgressProvider.setProgress(
progress: progress,
duration: _contentController.value.duration,
);
}
}
},
);
}
await _contentController.play();
}
Future<void> _pauseContent() {
setState(() {
_shouldShowContentVideo = false;
});
_contentProgressTimer?.cancel();
_contentProgressTimer = null;
return _contentController.pause();
}
@override
void dispose() {
_contentProgressTimer?.cancel();
_adsAttachment?.dispose();
unawaited(
_contentAttachmentFuture.then(
(attachment) => attachment.dispose(),
),
);
_adsManager?.destroy();
_contentController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Forge IMA example',
home: Scaffold(
appBar: AppBar(title: const Text('Forge IMA example')),
body: Center(
child: !_contentController.value.isInitialized
? const CircularProgressIndicator()
: AspectRatio(
aspectRatio: _contentController.value.aspectRatio,
child: Stack(
children: <Widget>[
// The display container must stay in the hierarchy for
// the whole session; it renders and handles ad clicks.
_adDisplayContainer,
if (_shouldShowContentVideo)
VideoPlayer(_contentController),
],
),
),
),
),
);
}
}