video_recommend_list_page.dart 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  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 'package:fluttertoast/fluttertoast.dart';
  9. import '../../model/video_new_model.dart';
  10. import '../../provider/video_commend_provider.dart';
  11. /// @author: bo.zeng
  12. /// @email: cnhbwds@gmail.com
  13. /// @date: 2025 2025/4/9 16:00
  14. /// @description:
  15. class VideoRecommendListPage extends ConsumerStatefulWidget {
  16. const VideoRecommendListPage({super.key});
  17. @override
  18. ConsumerState<VideoRecommendListPage> createState() =>
  19. _VideoRecommendListPageState();
  20. }
  21. final recommendListProvider =
  22. NotifierProvider<VideoRecommendProvider, List<VideoNewModel>>(() {
  23. return VideoRecommendProvider();
  24. });
  25. // 全局共享的视频控制器
  26. BetterPlayerController? globalVideoController;
  27. class _VideoRecommendListPageState extends ConsumerState<VideoRecommendListPage>
  28. with AutomaticKeepAliveClientMixin {
  29. late final PageController _pageController;
  30. int _currentPageIndex = 0;
  31. String? _currentVideoUrl;
  32. String? _lastFailedVideoUrl; // 记录上次失败的视频URL
  33. int _consecutiveFailures = 0; // 连续失败计数
  34. @override
  35. void initState() {
  36. super.initState();
  37. _pageController = PageController();
  38. // 先获取视频数据
  39. ref.read(recommendListProvider.notifier).fetchRecommendVideos();
  40. }
  41. // 安全地执行控制器操作
  42. void _safeControllerOperation(VoidCallback operation) {
  43. if (!mounted) return;
  44. final controller = globalVideoController;
  45. if (controller == null) return;
  46. try {
  47. if (controller.isVideoInitialized() == true) {
  48. operation();
  49. }
  50. } catch (e) {
  51. consoleLog("Controller operation error: $e");
  52. // 控制器可能已被释放,重置为 null
  53. globalVideoController = null;
  54. }
  55. }
  56. void _playVideo(String url) {
  57. consoleLog('===== _playVideo called with URL: $url =====');
  58. if (!mounted) return;
  59. // 检查现有控制器是否可用(未释放且是同一视频)
  60. if (_currentVideoUrl == url && globalVideoController != null) {
  61. try {
  62. if (globalVideoController!.isVideoInitialized() == true) {
  63. _safeControllerOperation(() {
  64. globalVideoController!.play();
  65. });
  66. return;
  67. }
  68. } catch (e) {
  69. consoleLog("Controller check failed, creating new one: $e");
  70. }
  71. }
  72. _currentVideoUrl = url;
  73. // 释放旧控制器
  74. try {
  75. globalVideoController?.dispose();
  76. } catch (e) {
  77. consoleLog("Dispose old controller error: $e");
  78. }
  79. globalVideoController = null;
  80. consoleLog("Creating BetterPlayerDataSource with url: $url");
  81. // 创建数据源(与 video_detail_page 一致)
  82. // 对于m4v格式,使用other格式让ExoPlayer自动检测
  83. final dataSource = BetterPlayerDataSource(
  84. BetterPlayerDataSourceType.network,
  85. url,
  86. videoFormat: BetterPlayerVideoFormat.other, // 让ExoPlayer自动检测格式
  87. notificationConfiguration: BetterPlayerNotificationConfiguration(
  88. showNotification: false,
  89. ),
  90. );
  91. // 创建新控制器,宽度占满、高度自适应
  92. globalVideoController = BetterPlayerController(
  93. BetterPlayerConfiguration(
  94. autoPlay: true,
  95. looping: true,
  96. fit: BoxFit.fitWidth, // 宽度占满,高度自适应
  97. controlsConfiguration: const BetterPlayerControlsConfiguration(
  98. showControls: false,
  99. ),
  100. handleLifecycle: false,
  101. autoDetectFullscreenAspectRatio: false,
  102. // 尝试使用软件解码作为后备
  103. errorBuilder: (context, errorMessage) {
  104. consoleLog("GlobalVideoController errorBuilder: $errorMessage");
  105. return Center(
  106. child: Text(
  107. "视频加载失败",
  108. style: TextStyle(color: Colors.white, fontSize: 14),
  109. ),
  110. );
  111. },
  112. ),
  113. betterPlayerDataSource: dataSource,
  114. );
  115. consoleLog("BetterPlayerController created");
  116. // 先添加事件监听
  117. globalVideoController!.addEventsListener((event) {
  118. consoleLog("GlobalVideoController event: ${event.betterPlayerEventType}");
  119. if (event.betterPlayerEventType == BetterPlayerEventType.exception) {
  120. // 打印详细异常信息
  121. consoleLog("Video exception occurred");
  122. consoleLog("Exception parameters: ${event.parameters}");
  123. consoleLog("Exception all data: ${event.toString()}");
  124. // 检查是否是硬件解码器不支持的错误
  125. final exception = event.parameters?['exception']?.toString() ?? '';
  126. if (exception.contains('NO_EXCEEDS_CAPABILITIES') ||
  127. exception.contains('MediaCodecVideoRenderer error')) {
  128. consoleLog("Hardware decoder not supported for this video");
  129. // 显示提示并跳过该视频
  130. if (mounted && _consecutiveFailures < 3) {
  131. Fluttertoast.showToast(
  132. msg: "该视频格式暂不支持,自动跳过",
  133. toastLength: Toast.LENGTH_SHORT,
  134. gravity: ToastGravity.CENTER,
  135. );
  136. // 延迟后跳到下一个视频
  137. Future.delayed(const Duration(milliseconds: 800), () {
  138. if (mounted && _currentPageIndex < ref.read(recommendListProvider).length - 1) {
  139. _consecutiveFailures++;
  140. _pageController.animateToPage(
  141. _currentPageIndex + 1,
  142. duration: const Duration(milliseconds: 300),
  143. curve: Curves.easeInOut,
  144. );
  145. } else {
  146. // 已经是最后一个视频了,重置计数
  147. _consecutiveFailures = 0;
  148. }
  149. });
  150. }
  151. }
  152. } else if (event.betterPlayerEventType == BetterPlayerEventType.play) {
  153. consoleLog("Video started playing");
  154. _consecutiveFailures = 0; // 播放成功,重置失败计数
  155. } else if (event.betterPlayerEventType == BetterPlayerEventType.bufferingStart) {
  156. consoleLog("Buffering started");
  157. } else if (event.betterPlayerEventType == BetterPlayerEventType.bufferingEnd) {
  158. consoleLog("Buffering ended");
  159. }
  160. });
  161. // 尝试播放
  162. Future.delayed(const Duration(milliseconds: 50), () {
  163. if (!mounted || globalVideoController == null) return;
  164. _safeControllerOperation(() {
  165. globalVideoController!.play();
  166. });
  167. });
  168. }
  169. @override
  170. void dispose() {
  171. _pageController.dispose();
  172. // 释放全局控制器
  173. try {
  174. globalVideoController?.dispose();
  175. } catch (e) {
  176. consoleLog("Dispose globalVideoController error: $e");
  177. }
  178. globalVideoController = null;
  179. super.dispose();
  180. }
  181. bool _isVisible = false;
  182. void _onVisibilityChanged(VisibilityInfo info) {
  183. if (!mounted) return;
  184. final visible = info.visibleFraction > 0.5;
  185. if (_isVisible != visible) {
  186. setState(() {
  187. _isVisible = visible;
  188. });
  189. }
  190. if (visible) {
  191. consoleLog("页面可见");
  192. // 页面重新可见时,恢复播放(仅在控制器已初始化时)
  193. try {
  194. if (globalVideoController != null &&
  195. globalVideoController!.isVideoInitialized() == true) {
  196. globalVideoController!.play();
  197. }
  198. } catch (e) {
  199. consoleLog("Visibility play error: $e");
  200. }
  201. } else {
  202. consoleLog("页面不可见");
  203. // 暂停播放
  204. try {
  205. if (globalVideoController != null &&
  206. globalVideoController!.isVideoInitialized() == true) {
  207. globalVideoController!.pause();
  208. }
  209. } catch (e) {
  210. consoleLog("Visibility pause error: $e");
  211. }
  212. }
  213. }
  214. void _onPageChanged(int index) {
  215. if (!mounted) return;
  216. setState(() {
  217. _currentPageIndex = index;
  218. });
  219. // 页面切换后,播放新视频
  220. final videos = ref.read(recommendListProvider);
  221. if (videos.isNotEmpty && index >= 0 && index < videos.length) {
  222. final url = videos[index].url;
  223. if (url != null && url.isNotEmpty) {
  224. _playVideo(url);
  225. }
  226. }
  227. }
  228. @override
  229. Widget build(BuildContext context) {
  230. super.build(context);
  231. final videos = ref.watch(recommendListProvider);
  232. // 初始化控制器并播放第一个视频
  233. if (videos.isNotEmpty && globalVideoController == null) {
  234. WidgetsBinding.instance.addPostFrameCallback((_) {
  235. final firstUrl = videos.firstOrNull?.url;
  236. if (firstUrl != null && firstUrl.isNotEmpty && mounted) {
  237. _playVideo(firstUrl);
  238. }
  239. });
  240. }
  241. // 视频列表为空时显示加载状态
  242. if (videos.isEmpty) {
  243. return const Center(
  244. child: CircularProgressIndicator(),
  245. );
  246. }
  247. return VisibilityDetector(
  248. key: const Key("value"),
  249. onVisibilityChanged: _onVisibilityChanged,
  250. child: Padding(
  251. padding: EdgeInsets.symmetric(
  252. horizontal: horizontalPadding,
  253. vertical: horizontalPadding,
  254. ),
  255. child: PageView.builder(
  256. controller: _pageController,
  257. onPageChanged: _onPageChanged,
  258. scrollDirection: Axis.vertical,
  259. itemCount: videos.length,
  260. itemBuilder: (context, index) {
  261. // 添加边界检查
  262. if (index < 0 || index >= videos.length) {
  263. return const SizedBox.shrink();
  264. }
  265. final item = videos[index];
  266. return VideoPlayItemWidget(
  267. key: ValueKey("video_${item.contentId ?? index}"),
  268. item: item,
  269. isActive: _isVisible && _currentPageIndex == index,
  270. index: index,
  271. );
  272. },
  273. ),
  274. ),
  275. );
  276. }
  277. @override
  278. bool get wantKeepAlive => true;
  279. }