mnemo_cards/games/ads_example/lib/native_player_page.dart
2025-11-11 02:55:41 +03:00

486 lines
No EOL
18 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:interactive_media_ads/interactive_media_ads.dart';
import 'package:video_player/video_player.dart';
/// Страница Native player с поддержкой VAST ads
class NativePlayerPage extends StatefulWidget {
/// Constructs a [NativePlayerPage].
const NativePlayerPage({super.key});
@override
State<NativePlayerPage> createState() => _NativePlayerPageState();
}
class _NativePlayerPageState extends State<NativePlayerPage>
with WidgetsBindingObserver {
// Last state received in `didChangeAppLifecycleState`.
AppLifecycleState _lastLifecycleState = AppLifecycleState.resumed;
// VAST URL для загрузки рекламы
static const String _adTagUrl =
'https://mediatoday.ru/c/ads.xml?pid=10548&vr=1&rid=5432234&dl=https://google.com';
// The AdsLoader instance exposes the request ads method.
late final AdsLoader _adsLoader;
// 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 = false;
// 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();
// Состояние рекламы
bool _isAdPlaying = false;
String _adStatus = '';
// Контроллер для ввода URL
late final TextEditingController _urlController = TextEditingController()
..text = _adTagUrl;
late final AdDisplayContainer _adDisplayContainer = AdDisplayContainer(
onContainerAdded: (AdDisplayContainer container) {
_adsLoader = AdsLoader(
container: container,
onAdsLoaded: (OnAdsLoadedData data) {
final AdsManager manager = data.manager;
_adsManager = data.manager;
manager.setAdsManagerDelegate(AdsManagerDelegate(
onAdEvent: (AdEvent event) {
debugPrint('OnAdEvent: ${event.type} => ${event.adData}');
// Формируем детальную информацию о событии
String eventInfo = 'Событие: ${event.type}';
if (event.adData != null) {
eventInfo += '\nДанные: ${event.adData}';
}
setState(() {
_adStatus = eventInfo;
});
switch (event.type) {
case AdEventType.loaded:
setState(() {
_adStatus = 'Реклама загружена\nДанные: ${event.adData ?? "Нет данных"}';
});
manager.start();
case AdEventType.contentPauseRequested:
setState(() {
_isAdPlaying = true;
_adStatus = 'Реклама началась\nДанные: ${event.adData ?? "Нет данных"}';
});
_pauseContent();
case AdEventType.contentResumeRequested:
setState(() {
_isAdPlaying = false;
_adStatus = 'Реклама завершена\nДанные: ${event.adData ?? "Нет данных"}';
});
_resumeContent();
case AdEventType.allAdsCompleted:
setState(() {
_isAdPlaying = false;
_adStatus = 'Все рекламы завершены\nДанные: ${event.adData ?? "Нет данных"}';
});
manager.destroy();
_adsManager = null;
case AdEventType.clicked:
setState(() {
_adStatus = 'Клик по рекламе\nДанные: ${event.adData ?? "Нет данных"}';
});
case AdEventType.tapped:
setState(() {
_adStatus = 'Тап по рекламе\nДанные: ${event.adData ?? "Нет данных"}';
});
case AdEventType.complete:
setState(() {
_adStatus = 'Реклама завершена\nДанные: ${event.adData ?? "Нет данных"}';
});
case AdEventType.paused:
setState(() {
_adStatus = 'Реклама на паузе\nДанные: ${event.adData ?? "Нет данных"}';
});
case AdEventType.resumed:
setState(() {
_adStatus = 'Реклама возобновлена\nДанные: ${event.adData ?? "Нет данных"}';
});
case _:
setState(() {
_adStatus = 'Событие: ${event.type}\nДанные: ${event.adData ?? "Нет данных"}';
});
}
},
onAdErrorEvent: (AdErrorEvent event) {
debugPrint('AdErrorEvent: ${event.error.message}');
setState(() {
_adStatus = 'Ошибка рекламы: ${event.error.message}\nКод: ${event.error.code}\nТип: ${event.error.type}';
_isAdPlaying = false;
});
_resumeContent();
},
));
manager.init(settings: AdsRenderingSettings(enablePreloading: true));
},
onAdsLoadError: (AdsLoadErrorData data) {
debugPrint('OnAdsLoadError: ${data.error.message}');
setState(() {
_adStatus = 'Ошибка загрузки рекламы: ${data.error.message}\nКод: ${data.error.code}\nТип: ${data.error.type}';
_isAdPlaying = false;
});
_resumeContent();
},
);
// Ads can't be requested until the `AdDisplayContainer` has been added to
// the native View hierarchy.
_requestAds(container);
},
);
@override
void initState() {
super.initState();
// Adds this instance as an observer for `AppLifecycleState` changes.
WidgetsBinding.instance.addObserver(this);
_contentVideoController = VideoPlayerController.networkUrl(
Uri.parse(
'https://storage.googleapis.com/gvabox/media/samples/stock.mp4',
),
)
..addListener(() {
if (_contentVideoController.value.isCompleted) {
_adsLoader.contentComplete();
}
setState(() {});
})
..initialize().then((_) {
// Ensure the first frame is shown after the video is initialized, even before the play button has been pressed.
setState(() {});
});
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
if (!_shouldShowContentVideo && _adsManager != null) {
// Возобновляем рекламу при возвращении в приложение
debugPrint('Resuming ad playback');
setState(() {
_adStatus = 'Возобновление рекламы...';
});
// Небольшая задержка для стабилизации состояния
Future.delayed(const Duration(milliseconds: 500), () {
try {
_adsManager!.resume();
setState(() {
_adStatus = 'Реклама возобновлена';
});
} catch (e) {
debugPrint('Error resuming ad: $e');
setState(() {
_adStatus = 'Ошибка возобновления рекламы: $e';
});
}
});
}
case AppLifecycleState.inactive:
// Не паузим рекламу при переходе по ссылке
debugPrint('App became inactive - keeping ad playing');
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
// Паузим рекламу только при полном закрытии приложения
if (!_shouldShowContentVideo && _adsManager != null) {
debugPrint('Pausing ad playback');
try {
_adsManager!.pause();
setState(() {
_adStatus = 'Реклама на паузе';
});
} catch (e) {
debugPrint('Error pausing ad: $e');
}
}
case AppLifecycleState.detached:
// Приложение полностью закрыто
debugPrint('App detached - cleaning up');
}
_lastLifecycleState = state;
}
@override
void dispose() {
_urlController.dispose();
_contentProgressTimer?.cancel();
_contentVideoController.dispose();
_adsManager?.destroy();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
Future<void> _requestAds(AdDisplayContainer container) {
return _adsLoader.requestAds(AdsRequest(
adTagUrl: _urlController.text.trim(),
contentProgressProvider: _contentProgressProvider,
));
}
Future<void> _resumeContent() async {
setState(() {
_shouldShowContentVideo = true;
});
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();
}
// Метод для принудительного возобновления рекламы
void _resumeAd() {
if (_adsManager != null && !_shouldShowContentVideo) {
debugPrint('Forcing ad resume');
try {
_adsManager!.resume();
setState(() {
_adStatus = 'Реклама принудительно возобновлена';
});
} catch (e) {
debugPrint('Error forcing ad resume: $e');
setState(() {
_adStatus = 'Ошибка возобновления рекламы: $e';
});
}
}
}
void _loadAdWithCustomUrl() {
final newUrl = _urlController.text.trim();
if (newUrl.isNotEmpty) {
setState(() {
_adStatus = 'Загрузка рекламы...';
_isAdPlaying = false;
_shouldShowContentVideo = false;
});
// Перезагружаем рекламу с новым URL
if (_adsManager != null) {
_adsManager!.destroy();
_adsManager = null;
}
// Запрашиваем новую рекламу
_requestAds(_adDisplayContainer);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Пожалуйста, введите URL'),
backgroundColor: Colors.red,
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Native Player'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Column(
children: [
// Поле ввода URL
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[300]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'URL для загрузки VAST рекламы:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 12),
TextField(
controller: _urlController,
decoration: const InputDecoration(
hintText: 'Введите URL VAST рекламы...',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _loadAdWithCustomUrl,
icon: const Icon(Icons.download),
label: const Text('Загрузить рекламу'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
),
),
const SizedBox(width: 12),
ElevatedButton.icon(
onPressed: () {
_urlController.text = _adTagUrl;
_loadAdWithCustomUrl();
},
icon: const Icon(Icons.refresh),
label: const Text('Сброс'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
foregroundColor: Colors.white,
),
),
],
),
const SizedBox(height: 8),
// Кнопка для принудительного возобновления рекламы
if (_adsManager != null && !_shouldShowContentVideo)
ElevatedButton.icon(
onPressed: _resumeAd,
icon: const Icon(Icons.play_arrow),
label: const Text('Возобновить рекламу'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
),
),
],
),
),
// Статус рекламы
if (_adStatus.isNotEmpty)
Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
_isAdPlaying ? Icons.play_arrow : Icons.info,
color: Colors.blue,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_adStatus,
style: const TextStyle(
color: Colors.blue,
fontWeight: FontWeight.w500,
fontSize: 12,
),
),
),
],
),
),
// Видео контейнер
Expanded(
child: Center(
child: SizedBox(
width: 300,
child: !_contentVideoController.value.isInitialized
? const Center(
child: CircularProgressIndicator(),
)
: AspectRatio(
aspectRatio: _contentVideoController.value.aspectRatio,
child: Stack(
children: <Widget>[
// The display container must be on screen before any Ads can be
// loaded and can't be removed between ads. This handles clicks for
// ads.
_adDisplayContainer,
if (_shouldShowContentVideo)
VideoPlayer(_contentVideoController)
],
),
),
),
),
),
],
),
floatingActionButton:
_contentVideoController.value.isInitialized && _shouldShowContentVideo
? FloatingActionButton(
onPressed: () {
setState(() {
_contentVideoController.value.isPlaying
? _contentVideoController.pause()
: _contentVideoController.play();
});
},
child: Icon(
_contentVideoController.value.isPlaying
? Icons.pause
: Icons.play_arrow,
),
)
: null,
);
}
}