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 createState() => _NativePlayerPageState(); } class _NativePlayerPageState extends State 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 _requestAds(AdDisplayContainer container) { return _adsLoader.requestAds(AdsRequest( adTagUrl: _urlController.text.trim(), contentProgressProvider: _contentProgressProvider, )); } Future _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 _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: [ // 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, ); } }