| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310 |
- import 'package:better_player_plus/better_player_plus.dart';
- import 'package:flutter/material.dart';
- import 'package:flutter_riverpod/flutter_riverpod.dart';
- import 'package:news_app/constant/size_res.dart';
- import 'package:news_app/ui/video/video_play_item_widget.dart';
- import 'package:news_app/util/log.util.dart';
- import 'package:visibility_detector/visibility_detector.dart';
- import 'package:fluttertoast/fluttertoast.dart';
- import '../../model/video_new_model.dart';
- import '../../provider/video_commend_provider.dart';
- /// @author: bo.zeng
- /// @email: cnhbwds@gmail.com
- /// @date: 2025 2025/4/9 16:00
- /// @description:
- class VideoRecommendListPage extends ConsumerStatefulWidget {
- const VideoRecommendListPage({super.key});
- @override
- ConsumerState<VideoRecommendListPage> createState() =>
- _VideoRecommendListPageState();
- }
- final recommendListProvider =
- NotifierProvider<VideoRecommendProvider, List<VideoNewModel>>(() {
- return VideoRecommendProvider();
- });
- // 全局共享的视频控制器
- BetterPlayerController? globalVideoController;
- class _VideoRecommendListPageState extends ConsumerState<VideoRecommendListPage>
- with AutomaticKeepAliveClientMixin {
- late final PageController _pageController;
- int _currentPageIndex = 0;
- String? _currentVideoUrl;
- String? _lastFailedVideoUrl; // 记录上次失败的视频URL
- int _consecutiveFailures = 0; // 连续失败计数
- @override
- void initState() {
- super.initState();
- _pageController = PageController();
- // 先获取视频数据
- ref.read(recommendListProvider.notifier).fetchRecommendVideos();
- }
- // 安全地执行控制器操作
- void _safeControllerOperation(VoidCallback operation) {
- if (!mounted) return;
- final controller = globalVideoController;
- if (controller == null) return;
- try {
- if (controller.isVideoInitialized() == true) {
- operation();
- }
- } catch (e) {
- consoleLog("Controller operation error: $e");
- // 控制器可能已被释放,重置为 null
- globalVideoController = null;
- }
- }
- void _playVideo(String url) {
- consoleLog('===== _playVideo called with URL: $url =====');
- if (!mounted) return;
- // 检查现有控制器是否可用(未释放且是同一视频)
- if (_currentVideoUrl == url && globalVideoController != null) {
- try {
- if (globalVideoController!.isVideoInitialized() == true) {
- _safeControllerOperation(() {
- globalVideoController!.play();
- });
- return;
- }
- } catch (e) {
- consoleLog("Controller check failed, creating new one: $e");
- }
- }
- _currentVideoUrl = url;
- // 释放旧控制器
- try {
- globalVideoController?.dispose();
- } catch (e) {
- consoleLog("Dispose old controller error: $e");
- }
- globalVideoController = null;
- consoleLog("Creating BetterPlayerDataSource with url: $url");
- // 创建数据源(与 video_detail_page 一致)
- // 对于m4v格式,使用other格式让ExoPlayer自动检测
- final dataSource = BetterPlayerDataSource(
- BetterPlayerDataSourceType.network,
- url,
- videoFormat: BetterPlayerVideoFormat.other, // 让ExoPlayer自动检测格式
- notificationConfiguration: BetterPlayerNotificationConfiguration(
- showNotification: false,
- ),
- );
- // 创建新控制器,宽度占满、高度自适应
- globalVideoController = BetterPlayerController(
- BetterPlayerConfiguration(
- autoPlay: true,
- looping: true,
- fit: BoxFit.fitWidth, // 宽度占满,高度自适应
- controlsConfiguration: const BetterPlayerControlsConfiguration(
- showControls: false,
- ),
- handleLifecycle: false,
- autoDetectFullscreenAspectRatio: false,
- // 尝试使用软件解码作为后备
- errorBuilder: (context, errorMessage) {
- consoleLog("GlobalVideoController errorBuilder: $errorMessage");
- return Center(
- child: Text(
- "视频加载失败",
- style: TextStyle(color: Colors.white, fontSize: 14),
- ),
- );
- },
- ),
- betterPlayerDataSource: dataSource,
- );
- consoleLog("BetterPlayerController created");
- // 先添加事件监听
- globalVideoController!.addEventsListener((event) {
- consoleLog("GlobalVideoController event: ${event.betterPlayerEventType}");
- if (event.betterPlayerEventType == BetterPlayerEventType.exception) {
- // 打印详细异常信息
- consoleLog("Video exception occurred");
- consoleLog("Exception parameters: ${event.parameters}");
- consoleLog("Exception all data: ${event.toString()}");
- // 检查是否是硬件解码器不支持的错误
- final exception = event.parameters?['exception']?.toString() ?? '';
- if (exception.contains('NO_EXCEEDS_CAPABILITIES') ||
- exception.contains('MediaCodecVideoRenderer error')) {
- consoleLog("Hardware decoder not supported for this video");
- // 显示提示并跳过该视频
- if (mounted && _consecutiveFailures < 3) {
- Fluttertoast.showToast(
- msg: "该视频格式暂不支持,自动跳过",
- toastLength: Toast.LENGTH_SHORT,
- gravity: ToastGravity.CENTER,
- );
- // 延迟后跳到下一个视频
- Future.delayed(const Duration(milliseconds: 800), () {
- if (mounted && _currentPageIndex < ref.read(recommendListProvider).length - 1) {
- _consecutiveFailures++;
- _pageController.animateToPage(
- _currentPageIndex + 1,
- duration: const Duration(milliseconds: 300),
- curve: Curves.easeInOut,
- );
- } else {
- // 已经是最后一个视频了,重置计数
- _consecutiveFailures = 0;
- }
- });
- }
- }
- } else if (event.betterPlayerEventType == BetterPlayerEventType.play) {
- consoleLog("Video started playing");
- _consecutiveFailures = 0; // 播放成功,重置失败计数
- } else if (event.betterPlayerEventType == BetterPlayerEventType.bufferingStart) {
- consoleLog("Buffering started");
- } else if (event.betterPlayerEventType == BetterPlayerEventType.bufferingEnd) {
- consoleLog("Buffering ended");
- }
- });
- // 尝试播放
- Future.delayed(const Duration(milliseconds: 50), () {
- if (!mounted || globalVideoController == null) return;
- _safeControllerOperation(() {
- globalVideoController!.play();
- });
- });
- }
- @override
- void dispose() {
- _pageController.dispose();
- // 释放全局控制器
- try {
- globalVideoController?.dispose();
- } catch (e) {
- consoleLog("Dispose globalVideoController error: $e");
- }
- globalVideoController = null;
- super.dispose();
- }
- bool _isVisible = false;
- void _onVisibilityChanged(VisibilityInfo info) {
- if (!mounted) return;
- final visible = info.visibleFraction > 0.5;
- if (_isVisible != visible) {
- setState(() {
- _isVisible = visible;
- });
- }
- if (visible) {
- consoleLog("页面可见");
- // 页面重新可见时,恢复播放(仅在控制器已初始化时)
- try {
- if (globalVideoController != null &&
- globalVideoController!.isVideoInitialized() == true) {
- globalVideoController!.play();
- }
- } catch (e) {
- consoleLog("Visibility play error: $e");
- }
- } else {
- consoleLog("页面不可见");
- // 暂停播放
- try {
- if (globalVideoController != null &&
- globalVideoController!.isVideoInitialized() == true) {
- globalVideoController!.pause();
- }
- } catch (e) {
- consoleLog("Visibility pause error: $e");
- }
- }
- }
- void _onPageChanged(int index) {
- if (!mounted) return;
- setState(() {
- _currentPageIndex = index;
- });
- // 页面切换后,播放新视频
- final videos = ref.read(recommendListProvider);
- if (videos.isNotEmpty && index >= 0 && index < videos.length) {
- final url = videos[index].url;
- if (url != null && url.isNotEmpty) {
- _playVideo(url);
- }
- }
- }
- @override
- Widget build(BuildContext context) {
- super.build(context);
- final videos = ref.watch(recommendListProvider);
- // 初始化控制器并播放第一个视频
- if (videos.isNotEmpty && globalVideoController == null) {
- WidgetsBinding.instance.addPostFrameCallback((_) {
- final firstUrl = videos.firstOrNull?.url;
- if (firstUrl != null && firstUrl.isNotEmpty && mounted) {
- _playVideo(firstUrl);
- }
- });
- }
- // 视频列表为空时显示加载状态
- if (videos.isEmpty) {
- return const Center(
- child: CircularProgressIndicator(),
- );
- }
- return VisibilityDetector(
- key: const Key("value"),
- onVisibilityChanged: _onVisibilityChanged,
- child: Padding(
- padding: EdgeInsets.symmetric(
- horizontal: horizontalPadding,
- vertical: horizontalPadding,
- ),
- child: PageView.builder(
- controller: _pageController,
- onPageChanged: _onPageChanged,
- scrollDirection: Axis.vertical,
- itemCount: videos.length,
- itemBuilder: (context, index) {
- // 添加边界检查
- if (index < 0 || index >= videos.length) {
- return const SizedBox.shrink();
- }
- final item = videos[index];
- return VideoPlayItemWidget(
- key: ValueKey("video_${item.contentId ?? index}"),
- item: item,
- isActive: _isVisible && _currentPageIndex == index,
- index: index,
- );
- },
- ),
- ),
- );
- }
- @override
- bool get wantKeepAlive => true;
- }
|