video_recommend_list_page.dart 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import 'package:better_player_plus/better_player_plus.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import 'package:news_app/constant/size_res.dart';
  5. import 'package:news_app/ui/video/video_play_item_widget.dart';
  6. import 'package:news_app/util/log.util.dart';
  7. import 'package:visibility_detector/visibility_detector.dart';
  8. import '../../model/video_new_model.dart';
  9. import '../../provider/video_commend_provider.dart';
  10. /// @author: bo.zeng
  11. /// @email: cnhbwds@gmail.com
  12. /// @date: 2025 2025/4/9 16:00
  13. /// @description:
  14. class VideoRecommendListPage extends ConsumerStatefulWidget {
  15. const VideoRecommendListPage({super.key});
  16. @override
  17. ConsumerState<VideoRecommendListPage> createState() =>
  18. _VideoRecommendListPageState();
  19. }
  20. final recommendListProvider =
  21. NotifierProvider<VideoRecommendProvider, List<VideoNewModel>>(() {
  22. return VideoRecommendProvider();
  23. });
  24. // 全局共享的视频控制器
  25. BetterPlayerController? globalVideoController;
  26. class _VideoRecommendListPageState extends ConsumerState<VideoRecommendListPage>
  27. with AutomaticKeepAliveClientMixin {
  28. late final PageController _pageController;
  29. int _currentPageIndex = 0;
  30. String? _currentVideoUrl;
  31. @override
  32. void initState() {
  33. super.initState();
  34. _pageController = PageController();
  35. // 先获取视频数据
  36. ref.read(recommendListProvider.notifier).fetchRecommendVideos();
  37. }
  38. // 安全地执行控制器操作
  39. void _safeControllerOperation(VoidCallback operation) {
  40. if (!mounted) return;
  41. final controller = globalVideoController;
  42. if (controller == null) return;
  43. try {
  44. if (controller.isVideoInitialized() == true) {
  45. operation();
  46. }
  47. } catch (e) {
  48. consoleLog("Controller operation error: $e");
  49. // 控制器可能已被释放,重置为 null
  50. globalVideoController = null;
  51. }
  52. }
  53. void _playVideo(String url) {
  54. if (!mounted) return;
  55. // 如果是同一个视频,不需要重新加载
  56. if (_currentVideoUrl == url && globalVideoController != null) {
  57. _safeControllerOperation(() {
  58. globalVideoController!.play();
  59. });
  60. return;
  61. }
  62. _currentVideoUrl = url;
  63. // 创建新控制器
  64. globalVideoController = null; // 释放旧的
  65. globalVideoController = BetterPlayerController(
  66. BetterPlayerConfiguration(
  67. autoPlay: true,
  68. looping: true,
  69. fit: BoxFit.fitWidth, // 宽度固定,高度自适应
  70. controlsConfiguration: const BetterPlayerControlsConfiguration(
  71. showControls: false,
  72. ),
  73. handleLifecycle: false,
  74. autoDetectFullscreenAspectRatio: false,
  75. errorBuilder: (context, errorMessage) {
  76. consoleLog("GlobalVideoController error: $errorMessage");
  77. // 返回空 widget,不显示错误
  78. return const SizedBox.shrink();
  79. },
  80. ),
  81. );
  82. // 先添加事件监听,然后再设置数据源
  83. globalVideoController!.addEventsListener((event) {
  84. consoleLog("GlobalVideoController event: ${event.betterPlayerEventType}");
  85. if (event.betterPlayerEventType == BetterPlayerEventType.exception) {
  86. consoleLog("Video exception occurred");
  87. // 静默处理异常
  88. }
  89. });
  90. Future.delayed(const Duration(milliseconds: 50), () {
  91. if (!mounted || globalVideoController == null) return;
  92. try {
  93. final dataSource = BetterPlayerDataSource(
  94. BetterPlayerDataSourceType.network,
  95. url,
  96. videoFormat: BetterPlayerVideoFormat.other,
  97. notificationConfiguration: BetterPlayerNotificationConfiguration(
  98. showNotification: false,
  99. ),
  100. );
  101. globalVideoController!.setupDataSource(dataSource).then((_) {
  102. if (mounted && globalVideoController != null) {
  103. _safeControllerOperation(() {
  104. globalVideoController!.play();
  105. });
  106. }
  107. }).catchError((error) {
  108. consoleLog("Video setup error: $error");
  109. // 静默处理错误,不显示错误提示
  110. });
  111. } catch (e) {
  112. consoleLog("Video play error: $e");
  113. // 静默处理错误,不显示错误提示
  114. }
  115. });
  116. }
  117. @override
  118. void dispose() {
  119. _pageController.dispose();
  120. // 暂停播放但不dispose,因为可能被其他页面使用
  121. globalVideoController?.pause();
  122. super.dispose();
  123. }
  124. bool _isVisible = false;
  125. void _onVisibilityChanged(VisibilityInfo info) {
  126. if (!mounted) return;
  127. final visible = info.visibleFraction > 0.5;
  128. if (_isVisible != visible) {
  129. setState(() {
  130. _isVisible = visible;
  131. });
  132. }
  133. if (visible) {
  134. consoleLog("页面可见");
  135. // 页面重新可见时,重新初始化视频以确保状态正确
  136. final videos = ref.read(recommendListProvider);
  137. if (videos.isNotEmpty && _currentPageIndex < videos.length) {
  138. final url = videos[_currentPageIndex].url;
  139. if (url != null && url.isNotEmpty) {
  140. _playVideo(url);
  141. }
  142. }
  143. } else {
  144. consoleLog("页面不可见");
  145. _safeControllerOperation(() {
  146. globalVideoController?.pause();
  147. });
  148. }
  149. }
  150. void _onPageChanged(int index) {
  151. if (!mounted) return;
  152. setState(() {
  153. _currentPageIndex = index;
  154. });
  155. // 页面切换后,播放新视频
  156. final videos = ref.read(recommendListProvider);
  157. if (videos.isNotEmpty && index >= 0 && index < videos.length) {
  158. final url = videos[index].url;
  159. if (url != null && url.isNotEmpty) {
  160. _playVideo(url);
  161. }
  162. }
  163. }
  164. @override
  165. Widget build(BuildContext context) {
  166. super.build(context);
  167. final videos = ref.watch(recommendListProvider);
  168. // 初始化控制器并播放第一个视频
  169. if (videos.isNotEmpty && globalVideoController == null) {
  170. WidgetsBinding.instance.addPostFrameCallback((_) {
  171. final firstUrl = videos.firstOrNull?.url;
  172. if (firstUrl != null && firstUrl.isNotEmpty && mounted) {
  173. _playVideo(firstUrl);
  174. }
  175. });
  176. }
  177. // 视频列表为空时显示加载状态
  178. if (videos.isEmpty) {
  179. return const Center(
  180. child: CircularProgressIndicator(),
  181. );
  182. }
  183. return VisibilityDetector(
  184. key: const Key("value"),
  185. onVisibilityChanged: _onVisibilityChanged,
  186. child: Padding(
  187. padding: EdgeInsets.symmetric(
  188. horizontal: horizontalPadding,
  189. vertical: horizontalPadding,
  190. ),
  191. child: PageView.builder(
  192. controller: _pageController,
  193. onPageChanged: _onPageChanged,
  194. scrollDirection: Axis.vertical,
  195. itemCount: videos.length,
  196. itemBuilder: (context, index) {
  197. // 添加边界检查
  198. if (index < 0 || index >= videos.length) {
  199. return const SizedBox.shrink();
  200. }
  201. final item = videos[index];
  202. return VideoPlayItemWidget(
  203. key: ValueKey("video_${item.contentId ?? index}"),
  204. item: item,
  205. isActive: _isVisible && _currentPageIndex == index,
  206. index: index,
  207. );
  208. },
  209. ),
  210. ),
  211. );
  212. }
  213. @override
  214. bool get wantKeepAlive => true;
  215. }