| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  | import 'dart:io'; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | import 'package:flutter/material.dart'; | 
					
						
							|  |  |  |  | import 'package:video_player/video_player.dart'; | 
					
						
							| 
									
										
										
										
											2025-08-19 11:06:16 +08:00
										 |  |  |  | import 'package:chewie/chewie.dart'; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  | /// 弹窗组件:支持 本地文件 / 网络视频 自动识别
 | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | class VideoPlayerPopup extends StatefulWidget { | 
					
						
							|  |  |  |  |   final String videoUrl; | 
					
						
							|  |  |  |  |   const VideoPlayerPopup({Key? key, required this.videoUrl}) : super(key: key); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							|  |  |  |  |   State<VideoPlayerPopup> createState() => _VideoPlayerPopupState(); | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | class _VideoPlayerPopupState extends State<VideoPlayerPopup> { | 
					
						
							| 
									
										
										
										
											2025-08-19 11:06:16 +08:00
										 |  |  |  |   late VideoPlayerController _videoController; | 
					
						
							|  |  |  |  |   ChewieController? _chewieController; | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |   bool _isNetwork = false; | 
					
						
							|  |  |  |  |   bool _initializing = true; | 
					
						
							|  |  |  |  |   String? _error; | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							|  |  |  |  |   void initState() { | 
					
						
							|  |  |  |  |     super.initState(); | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |     _initController(); | 
					
						
							|  |  |  |  |   } | 
					
						
							| 
									
										
										
										
											2025-08-19 11:06:16 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |   Future<void> _initController() async { | 
					
						
							|  |  |  |  |     try { | 
					
						
							|  |  |  |  |       final uri = Uri.tryParse(widget.videoUrl); | 
					
						
							|  |  |  |  |       final scheme = uri?.scheme?.toLowerCase() ?? ''; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       // 判定是否网络视频(包括 http/https/rtsp/rtmp)
 | 
					
						
							|  |  |  |  |       _isNetwork = (scheme == 'http' || scheme == 'https' || scheme == 'rtsp' || scheme == 'rtmp'); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       if (_isNetwork) { | 
					
						
							|  |  |  |  |         // 网络视频
 | 
					
						
							|  |  |  |  |         _videoController = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); | 
					
						
							|  |  |  |  |       } else { | 
					
						
							|  |  |  |  |         // 本地视频:支持 file:// 开头或直接是本地路径
 | 
					
						
							|  |  |  |  |         if (scheme == 'file' && uri != null) { | 
					
						
							|  |  |  |  |           _videoController = VideoPlayerController.file(File(uri.toFilePath())); | 
					
						
							|  |  |  |  |         } else { | 
					
						
							|  |  |  |  |           // 直接当做本地路径处理(例如 /storage/... 或 沙盒内路径)
 | 
					
						
							|  |  |  |  |           _videoController = VideoPlayerController.file(File(widget.videoUrl)); | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       // 初始化 VideoPlayerController
 | 
					
						
							|  |  |  |  |       await _videoController.initialize(); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       // 在视频初始化完成后创建 ChewieController(以确保 aspectRatio 可用)
 | 
					
						
							|  |  |  |  |       _chewieController?.dispose(); | 
					
						
							|  |  |  |  |       _chewieController = ChewieController( | 
					
						
							|  |  |  |  |         videoPlayerController: _videoController, | 
					
						
							|  |  |  |  |         autoPlay: true, | 
					
						
							|  |  |  |  |         looping: false, | 
					
						
							|  |  |  |  |         showOptions: false, | 
					
						
							|  |  |  |  |         allowFullScreen: true, | 
					
						
							|  |  |  |  |         allowPlaybackSpeedChanging: true, | 
					
						
							|  |  |  |  |         allowMuting: true, | 
					
						
							|  |  |  |  |         showControlsOnInitialize: true, | 
					
						
							|  |  |  |  |         // 不要在这里强制颜色(你可以自定义),但保留示例:
 | 
					
						
							|  |  |  |  |         materialProgressColors: ChewieProgressColors( | 
					
						
							|  |  |  |  |           playedColor: Colors.blue, | 
					
						
							|  |  |  |  |           backgroundColor: Colors.white, | 
					
						
							|  |  |  |  |           handleColor: Colors.blue, | 
					
						
							| 
									
										
										
										
											2025-09-01 17:25:55 +08:00
										 |  |  |  |           bufferedColor: Colors.white, | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |         ), | 
					
						
							|  |  |  |  |         aspectRatio: _videoController.value.aspectRatio > 0 | 
					
						
							|  |  |  |  |             ? _videoController.value.aspectRatio | 
					
						
							|  |  |  |  |             : 16 / 9, | 
					
						
							|  |  |  |  |         errorBuilder: (context, errorMessage) { | 
					
						
							|  |  |  |  |           return Center( | 
					
						
							|  |  |  |  |             child: Text( | 
					
						
							|  |  |  |  |               errorMessage ?? '视频播放错误', | 
					
						
							|  |  |  |  |               style: const TextStyle(color: Colors.white), | 
					
						
							|  |  |  |  |             ), | 
					
						
							|  |  |  |  |           ); | 
					
						
							|  |  |  |  |         }, | 
					
						
							|  |  |  |  |       ); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       if (!mounted) return; | 
					
						
							|  |  |  |  |       setState(() { | 
					
						
							|  |  |  |  |         _initializing = false; | 
					
						
							|  |  |  |  |       }); | 
					
						
							|  |  |  |  |     } catch (e, st) { | 
					
						
							|  |  |  |  |       // 捕获异常并展示错误信息
 | 
					
						
							|  |  |  |  |       debugPrint('Video init error: $e\n$st'); | 
					
						
							|  |  |  |  |       if (!mounted) return; | 
					
						
							|  |  |  |  |       setState(() { | 
					
						
							|  |  |  |  |         _initializing = false; | 
					
						
							|  |  |  |  |         _error = e.toString(); | 
					
						
							|  |  |  |  |       }); | 
					
						
							|  |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							|  |  |  |  |   void dispose() { | 
					
						
							| 
									
										
										
										
											2025-08-19 11:06:16 +08:00
										 |  |  |  |     _chewieController?.dispose(); | 
					
						
							|  |  |  |  |     _videoController.dispose(); | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |     super.dispose(); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							|  |  |  |  |   Widget build(BuildContext context) { | 
					
						
							|  |  |  |  |     return Center( | 
					
						
							|  |  |  |  |       child: Material( | 
					
						
							|  |  |  |  |         color: Colors.transparent, | 
					
						
							|  |  |  |  |         child: Container( | 
					
						
							|  |  |  |  |           constraints: BoxConstraints( | 
					
						
							|  |  |  |  |             maxWidth: MediaQuery.of(context).size.width * 0.9, | 
					
						
							| 
									
										
										
										
											2025-08-19 11:06:16 +08:00
										 |  |  |  |             maxHeight: 500, | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |           ), | 
					
						
							|  |  |  |  |           decoration: BoxDecoration( | 
					
						
							| 
									
										
										
										
											2025-08-19 11:06:16 +08:00
										 |  |  |  |             color: Colors.black, | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |             borderRadius: BorderRadius.circular(8), | 
					
						
							|  |  |  |  |           ), | 
					
						
							|  |  |  |  |           child: Stack( | 
					
						
							|  |  |  |  |             children: [ | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |               // 视频播放器 或 错误 / 加载指示
 | 
					
						
							|  |  |  |  |               Positioned.fill( | 
					
						
							|  |  |  |  |                 child: _buildPlayerBody(), | 
					
						
							|  |  |  |  |               ), | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |               // 关闭按钮
 | 
					
						
							|  |  |  |  |               Positioned( | 
					
						
							|  |  |  |  |                 top: 4, | 
					
						
							|  |  |  |  |                 right: 4, | 
					
						
							|  |  |  |  |                 child: IconButton( | 
					
						
							| 
									
										
										
										
											2025-08-19 11:06:16 +08:00
										 |  |  |  |                   icon: const Icon(Icons.close, color: Colors.white), | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |                   onPressed: () { | 
					
						
							|  |  |  |  |                     Navigator.of(context).pop(); | 
					
						
							|  |  |  |  |                   }, | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  |                 ), | 
					
						
							|  |  |  |  |               ), | 
					
						
							|  |  |  |  |             ], | 
					
						
							|  |  |  |  |           ), | 
					
						
							|  |  |  |  |         ), | 
					
						
							|  |  |  |  |       ), | 
					
						
							|  |  |  |  |     ); | 
					
						
							|  |  |  |  |   } | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |   Widget _buildPlayerBody() { | 
					
						
							|  |  |  |  |     if (_initializing) { | 
					
						
							|  |  |  |  |       return const Center(child: CircularProgressIndicator()); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     if (_error != null) { | 
					
						
							|  |  |  |  |       return Center( | 
					
						
							|  |  |  |  |         child: Padding( | 
					
						
							|  |  |  |  |           padding: const EdgeInsets.all(16.0), | 
					
						
							|  |  |  |  |           child: Text( | 
					
						
							|  |  |  |  |             '播放失败:$_error', | 
					
						
							|  |  |  |  |             style: const TextStyle(color: Colors.white), | 
					
						
							|  |  |  |  |           ), | 
					
						
							|  |  |  |  |         ), | 
					
						
							|  |  |  |  |       ); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     if (_chewieController != null && _videoController.value.isInitialized) { | 
					
						
							|  |  |  |  |       return Chewie(controller: _chewieController!); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     return const Center(child: Text('无法播放视频', style: TextStyle(color: Colors.white))); | 
					
						
							|  |  |  |  |   } | 
					
						
							| 
									
										
										
										
											2025-07-11 11:03:21 +08:00
										 |  |  |  | } |