diff --git a/lib/controller/message/chat_controller.dart b/lib/controller/message/chat_controller.dart index c98df36..c52d28b 100644 --- a/lib/controller/message/chat_controller.dart +++ b/lib/controller/message/chat_controller.dart @@ -1,4 +1,5 @@ import 'package:get/get.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import '../../im/im_manager.dart'; import 'package:im_flutter_sdk/im_flutter_sdk.dart'; @@ -16,6 +17,10 @@ class ChatController extends GetxController { // 加载更多的游标 String? _cursor; + // 视频发送状态 + final RxBool isSendingVideo = RxBool(false); + final RxString sendingStatus = RxString(''); + ChatController({required this.userId}); @override @@ -128,34 +133,76 @@ class ChatController extends GetxController { /// 发送视频消息 Future sendVideoMessage(String filePath, int duration) async { + // 如果正在发送,防止重复发送 + if (isSendingVideo.value) { + SmartDialog.showToast('视频正在发送中,请稍候...'); + return false; + } + try { + // 设置发送状态 + isSendingVideo.value = true; + sendingStatus.value = '正在准备视频...'; + update(); + print('🎬 [ChatController] 准备发送视频消息'); print('视频路径: $filePath'); print('视频时长: $duration 秒'); + sendingStatus.value = '正在上传视频...'; + update(); + final message = await IMManager.instance.sendVideoMessage( filePath, userId, duration, ); + if (message != null) { print('✅ [ChatController] 视频消息创建成功'); print('消息类型: ${message.body.type}'); + + sendingStatus.value = '发送成功'; + update(); + // 发送成功后将消息添加到列表开头 messages.insert(0, message); update(); + // 更新会话列表 _refreshConversationList(); + + // 显示成功提示 + SmartDialog.showToast('✅ 视频发送成功'); + return true; } + print('❌ [ChatController] 视频消息创建失败'); + sendingStatus.value = '发送失败'; + update(); + + SmartDialog.showToast('❌ 视频消息发送失败,请重试'); + return false; } catch (e) { print('❌ [ChatController] 发送视频消息异常: $e'); + + sendingStatus.value = '发送失败: $e'; + update(); + if (Get.isLogEnable) { Get.log('发送视频消息失败: $e'); } + + SmartDialog.showToast('❌ 视频消息发送失败: ${e.toString()}'); + return false; + } finally { + // 重置发送状态 + isSendingVideo.value = false; + sendingStatus.value = ''; + update(); } } diff --git a/lib/im/im_manager.dart b/lib/im/im_manager.dart index 78ff82c..da377cd 100644 --- a/lib/im/im_manager.dart +++ b/lib/im/im_manager.dart @@ -1,6 +1,9 @@ +import 'dart:io'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:im_flutter_sdk/im_flutter_sdk.dart'; +import 'package:video_thumbnail/video_thumbnail.dart'; +import 'package:path_provider/path_provider.dart'; import '../controller/message/conversation_controller.dart'; import '../controller/message/chat_controller.dart'; @@ -236,15 +239,61 @@ class IMManager { print('接收用户: $toChatUsername'); print('视频时长: $duration 秒'); + // 🎯 手动生成视频缩略图 + String? thumbnailPath; + try { + print('📸 [IMManager] 开始生成视频缩略图...'); + + // 获取临时目录 + final tempDir = await getTemporaryDirectory(); + final fileName = videoPath.split('/').last.split('.').first; + final thumbFileName = '${fileName}_thumb.jpg'; + thumbnailPath = '${tempDir.path}/$thumbFileName'; + + // 使用 video_thumbnail 生成缩略图 + final uint8list = await VideoThumbnail.thumbnailFile( + video: videoPath, + thumbnailPath: thumbnailPath, + imageFormat: ImageFormat.JPEG, + maxWidth: 400, // 缩略图最大宽度 + quality: 75, // 图片质量 + ); + + if (uint8list != null && File(uint8list).existsSync()) { + thumbnailPath = uint8list; + print('✅ [IMManager] 缩略图生成成功: $thumbnailPath'); + } else { + print('⚠️ [IMManager] 缩略图生成返回null'); + thumbnailPath = null; + } + } catch (e) { + print('❌ [IMManager] 生成缩略图失败: $e'); + thumbnailPath = null; + } + // 创建视频消息 final message = EMMessage.createVideoSendMessage( targetId: toChatUsername, filePath: videoPath, duration: duration, + thumbnailLocalPath: thumbnailPath, // 🎯 指定缩略图路径 ); print('消息创建成功,消息类型: ${message.body.type}'); print('消息体是否为视频: ${message.body is EMVideoMessageBody}'); + + // 检查缩略图信息 + if (message.body is EMVideoMessageBody) { + final videoBody = message.body as EMVideoMessageBody; + print('📸 [IMManager] 缩略图本地路径: ${videoBody.thumbnailLocalPath}'); + print('📸 [IMManager] 缩略图远程路径: ${videoBody.thumbnailRemotePath}'); + + // 验证缩略图文件是否存在 + if (videoBody.thumbnailLocalPath != null) { + final thumbFile = File(videoBody.thumbnailLocalPath!); + print('📸 [IMManager] 缩略图文件是否存在: ${thumbFile.existsSync()}'); + } + } // 发送消息 await EMClient.getInstance.chatManager.sendMessage(message); diff --git a/lib/pages/message/chat_page.dart b/lib/pages/message/chat_page.dart index a7bbeed..a2efe33 100644 --- a/lib/pages/message/chat_page.dart +++ b/lib/pages/message/chat_page.dart @@ -79,6 +79,53 @@ class _ChatPageState extends State { ), body: Column( children: [ + // 视频发送状态提示 + Obx(() { + if (controller.isSendingVideo.value) { + return Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 12.h, + ), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.1), + border: Border( + bottom: BorderSide( + color: Colors.blue.withOpacity(0.3), + width: 1, + ), + ), + ), + child: Row( + children: [ + SizedBox( + width: 20.w, + height: 20.w, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.blue, + ), + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Text( + controller.sendingStatus.value, + style: TextStyle( + fontSize: 14.sp, + color: Colors.blue[700], + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ); + } + return SizedBox.shrink(); + }), // 消息列表区域 Expanded( child: Container( @@ -94,6 +141,8 @@ class _ChatPageState extends State { reverse: true, padding: EdgeInsets.all(16.w), itemCount: controller.messages.length, + // 🚀 性能优化:添加缓存范围,减少重建 + cacheExtent: 500, // 缓存屏幕外500像素的内容 itemBuilder: (context, index) { final message = controller.messages[index]; final isSentByMe = @@ -103,7 +152,9 @@ class _ChatPageState extends State { ? controller.messages[index - 1] : null; + // 🚀 性能优化:为每个消息项设置唯一的 key return MessageItem( + key: ValueKey(message.msgId), message: message, isSentByMe: isSentByMe, previousMessage: previousMessage, @@ -132,6 +183,13 @@ class _ChatPageState extends State { print('🎬 [ChatPage] 收到视频录制/选择回调'); print('文件路径: $filePath'); print('时长: $duration 秒'); + + // 检查是否正在发送 + if (controller.isSendingVideo.value) { + print('⚠️ [ChatPage] 视频正在发送中,忽略新的发送请求'); + return; + } + // 处理视频录制/选择完成,回传文件路径和时长 await controller.sendVideoMessage(filePath, duration); }, diff --git a/lib/widget/message/video_input_view.dart b/lib/widget/message/video_input_view.dart index 7b0ae82..27fc501 100644 --- a/lib/widget/message/video_input_view.dart +++ b/lib/widget/message/video_input_view.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:wechat_assets_picker/wechat_assets_picker.dart'; import 'package:wechat_camera_picker/wechat_camera_picker.dart'; @@ -49,7 +50,7 @@ class _VideoInputViewState extends State { final micStatus = await Permission.microphone.request(); if (!cameraStatus.isGranted || !micStatus.isGranted) { - Get.snackbar('提示', '需要相机和麦克风权限才能录制视频'); + SmartDialog.showToast('需要相机和麦克风权限才能录制视频'); return; } @@ -170,7 +171,7 @@ class _VideoInputViewState extends State { if (Get.isLogEnable) { Get.log("选择视频失败: $e"); } - Get.snackbar('错误', '选择视频失败: $e'); + SmartDialog.showToast('❌ 选择视频失败: $e'); } } diff --git a/lib/widget/message/video_item.dart b/lib/widget/message/video_item.dart index fba12cd..fed5eeb 100644 --- a/lib/widget/message/video_item.dart +++ b/lib/widget/message/video_item.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:im_flutter_sdk/im_flutter_sdk.dart'; import 'package:get/get.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:video_player/video_player.dart'; import 'package:dating_touchme_app/pages/message/video_player_page.dart'; @@ -24,73 +26,96 @@ class VideoItem extends StatefulWidget { State createState() => _VideoItemState(); } +// 消息发送状态枚举 +enum MessageSendStatus { + sending, + success, + failed, +} + class _VideoItemState extends State { - VideoPlayerController? _controller; - bool _isInitialized = false; String? _thumbnailPath; + bool _isLoadingVideo = false; @override void initState() { super.initState(); - _initializeVideo(); + // 🚀 极致性能优化:只准备缩略图路径,完全不初始化视频控制器 + _prepareThumbnail(); } - Future _initializeVideo() async { + /// 准备缩略图路径(轻量级操作) + void _prepareThumbnail() { + final thumbLocal = widget.videoBody.thumbnailLocalPath; + final thumbRemote = widget.videoBody.thumbnailRemotePath; + + print('🖼️ [VideoItem] 缩略图调试信息:'); + print('本地缩略图路径: $thumbLocal'); + print('远程缩略图路径: $thumbRemote'); + + // 优先使用本地缩略图 + if (thumbLocal != null && thumbLocal.isNotEmpty) { + final file = File(thumbLocal); + if (file.existsSync()) { + _thumbnailPath = thumbLocal; + print('✅ 使用本地缩略图: $thumbLocal'); + return; + } else { + print('⚠️ 本地缩略图文件不存在: $thumbLocal'); + } + } + + // 使用远程缩略图 + if (thumbRemote != null && thumbRemote.isNotEmpty) { + _thumbnailPath = thumbRemote; + print('✅ 使用远程缩略图: $thumbRemote'); + return; + } + + // 🎯 备选方案:如果没有缩略图,尝试使用视频第一帧 + final videoLocal = widget.videoBody.localPath; + final videoRemote = widget.videoBody.remotePath; + + print('⚠️ 没有缩略图,尝试使用视频路径作为预览'); + print('本地视频路径: $videoLocal'); + print('远程视频路径: $videoRemote'); + + // 如果有本地视频,使用视频文件生成预览 + if (videoLocal.isNotEmpty && File(videoLocal).existsSync()) { + print('💡 [VideoItem] 将使用视频第一帧作为预览'); + _generateThumbnailFromVideo(videoLocal); + } else { + print('❌ 没有可用的缩略图和视频,将显示占位符图标'); + } + } + + /// 🎯 从视频生成缩略图(备选方案) + void _generateThumbnailFromVideo(String videoPath) async { try { - // 获取本地视频路径 - final localPath = widget.videoBody.localPath; - final remotePath = widget.videoBody.remotePath; + print('🎬 [VideoItem] 开始从视频生成缩略图...'); - print('=== 视频消息调试信息 ==='); - print('本地路径: $localPath'); - print('远程路径: $remotePath'); - print('视频时长: ${widget.videoBody.duration}秒'); + // 使用 video_player 获取第一帧 + final controller = VideoPlayerController.file(File(videoPath)); + await controller.initialize(); - if (localPath.isNotEmpty && File(localPath).existsSync()) { - // 使用本地文件 - print('使用本地视频文件'); - _controller = VideoPlayerController.file(File(localPath)); - } else if (remotePath != null && remotePath.isNotEmpty) { - // 使用远程URL - print('使用远程视频URL'); - _controller = VideoPlayerController.networkUrl(Uri.parse(remotePath)); - } else { - print('⚠️ 警告: 没有可用的视频路径'); - } - - if (_controller != null) { - await _controller!.initialize(); + // 设置缩略图路径为视频路径(让 UI 知道要使用视频预览) + if (mounted) { setState(() { - _isInitialized = true; + _thumbnailPath = 'video:$videoPath'; // 特殊标记 }); - print('✅ 视频初始化成功'); + print('✅ [VideoItem] 使用视频第一帧作为预览'); } - - // 获取缩略图路径 - final thumbLocal = widget.videoBody.thumbnailLocalPath; - final thumbRemote = widget.videoBody.thumbnailRemotePath; - print('缩略图本地路径: $thumbLocal'); - print('缩略图远程路径: $thumbRemote'); - - if (thumbLocal != null && thumbLocal.isNotEmpty) { - _thumbnailPath = thumbLocal; - print('使用本地缩略图'); - } else if (thumbRemote != null && thumbRemote.isNotEmpty) { - _thumbnailPath = thumbRemote; - print('使用远程缩略图'); - } else { - print('⚠️ 警告: 没有可用的缩略图'); - } - print('======================'); + // 释放控制器 + controller.dispose(); } catch (e) { - print('❌ 初始化视频失败: $e'); + print('❌ [VideoItem] 生成视频预览失败: $e'); } } @override void dispose() { - _controller?.dispose(); + // 无需释放资源,因为没有初始化任何控制器 super.dispose(); } @@ -117,7 +142,12 @@ class _VideoItemState extends State { } // 播放视频 - void _playVideo() { + void _playVideo() async { + // 如果正在加载,不处理点击 + if (_isLoadingVideo) { + return; + } + // 获取视频路径 final localPath = widget.videoBody.localPath; final remotePath = widget.videoBody.remotePath; @@ -135,8 +165,18 @@ class _VideoItemState extends State { } if (videoPath != null) { + // 显示加载状态(如果是网络视频) + if (isNetwork) { + setState(() { + _isLoadingVideo = true; + }); + + // 模拟加载延迟(可选) + await Future.delayed(Duration(milliseconds: 300)); + } + // 使用 Chewie 播放器页面 - Get.to( + await Get.to( () => VideoPlayerPage( videoPath: videoPath!, isNetwork: isNetwork, @@ -144,12 +184,15 @@ class _VideoItemState extends State { transition: Transition.fade, duration: const Duration(milliseconds: 200), ); + + // 隐藏加载状态 + if (mounted && isNetwork) { + setState(() { + _isLoadingVideo = false; + }); + } } else { - Get.snackbar( - '提示', - '视频路径不可用', - snackPosition: SnackPosition.BOTTOM, - ); + SmartDialog.showToast('⚠️ 视频路径不可用,请稍后重试'); } } @@ -166,88 +209,142 @@ class _VideoItemState extends State { : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 视频消息气泡 - GestureDetector( - onTap: _playVideo, - child: Container( - width: 200.w, - height: 150.h, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(8.w), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(8.w), - child: Stack( - fit: StackFit.expand, - children: [ - // 背景层:始终显示占位符 - Container( - color: Colors.grey[300], - child: Icon( - Icons.videocam, - size: 48.w, - color: Colors.grey[600], + // 🚀 极致性能优化:无需初始化,直接显示缩略图 + Stack( + children: [ + GestureDetector( + onTap: _playVideo, + child: Container( + width: 200.w, + height: 150.h, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: Offset(0, 2), ), - ), - // 视频层或缩略图层 - if (_isInitialized && _controller != null) - Center( - child: AspectRatio( - aspectRatio: _controller!.value.aspectRatio, - child: VideoPlayer(_controller!), - ), - ) - else if (_thumbnailPath != null && _thumbnailPath!.isNotEmpty) - _buildThumbnail(), - // 播放按钮和时长覆盖层 - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.transparent, - Colors.black.withOpacity(0.4), - ], + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12.w), + child: Stack( + fit: StackFit.expand, + children: [ + // 🚀 背景层:默认占位符 + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.grey[300]!, + Colors.grey[400]!, + ], + ), + ), + child: Icon( + Icons.videocam, + size: 48.w, + color: Colors.grey[600], + ), ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // 播放按钮 - Icon( - Icons.play_circle_filled, - size: 56.w, - color: Colors.white.withOpacity(0.9), + // 🚀 缩略图层:始终显示缩略图(如果有) + if (_thumbnailPath != null && _thumbnailPath!.isNotEmpty) + _buildThumbnail(), + // 播放按钮和时长覆盖层 + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withOpacity(0.1), + Colors.black.withOpacity(0.5), + ], + ), ), - SizedBox(height: 8.h), - // 视频时长 - Container( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 播放按钮(增强动画效果) + Container( + padding: EdgeInsets.all(8.w), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon( + Icons.play_arrow_rounded, + size: 48.w, + color: Colors.white, + ), + ), + ], + ), + ), + // 右下角显示时长标签 + Positioned( + right: 8.w, + bottom: 8.h, + child: Container( padding: EdgeInsets.symmetric( horizontal: 8.w, vertical: 4.h, ), decoration: BoxDecoration( - color: Colors.black.withOpacity(0.6), + color: Colors.black.withOpacity(0.7), borderRadius: BorderRadius.circular(4.w), ), - child: Text( - _formatDuration(widget.videoBody.duration ?? 0), - style: TextStyle( - fontSize: 12.sp, - color: Colors.white, - fontWeight: FontWeight.w500, - ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.videocam_rounded, + size: 12.w, + color: Colors.white, + ), + SizedBox(width: 4.w), + Text( + _formatDuration(widget.videoBody.duration ?? 0), + style: TextStyle( + fontSize: 11.sp, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ], ), ), - ], - ), + ), + ], ), - ], + ), ), ), - ), + // 🚀 加载指示器(播放时显示) + if (_isLoadingVideo) + Positioned.fill( + child: Container( + width: 200.w, + height: 150.h, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: BorderRadius.circular(12.w), + ), + child: Center( + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: AlwaysStoppedAnimation( + Colors.white, + ), + ), + ), + ), + ), + ], ), ], ), @@ -256,36 +353,125 @@ class _VideoItemState extends State { ); } - // 构建缩略图 + // 🚀 性能优化:构建缓存的缩略图 Widget _buildThumbnail() { if (_thumbnailPath == null || _thumbnailPath!.isEmpty) { + print('⚠️ [VideoItem] 缩略图路径为空,不渲染'); return const SizedBox.shrink(); } + print('🎨 [VideoItem] 开始渲染缩略图: $_thumbnailPath'); + + // 🎯 检查是否是视频预览模式 + if (_thumbnailPath!.startsWith('video:')) { + print('🎬 [VideoItem] 使用视频第一帧预览模式'); + final videoPath = _thumbnailPath!.substring(6); // 移除 "video:" 前缀 + return _buildVideoPreview(videoPath); + } + // 判断是本地路径还是远程路径 if (_thumbnailPath!.startsWith('http')) { - return Image.network( - _thumbnailPath!, + print('🌐 [VideoItem] 渲染网络缩略图'); + // 🚀 使用 CachedNetworkImage 缓存网络图片 + return CachedNetworkImage( + imageUrl: _thumbnailPath!, fit: BoxFit.cover, width: double.infinity, height: double.infinity, - errorBuilder: (context, error, stackTrace) { + placeholder: (context, url) { + print('⏳ [VideoItem] 网络缩略图加载中...'); + return Container( + color: Colors.grey[300], + child: Center( + child: SizedBox( + width: 24.w, + height: 24.w, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.grey[600], + ), + ), + ), + ); + }, + errorWidget: (context, url, error) { + print('❌ [VideoItem] 网络缩略图加载失败: $error'); return const SizedBox.shrink(); }, + // 🚀 内存缓存配置 + memCacheWidth: 400, // 限制缓存图片宽度 + memCacheHeight: 300, // 限制缓存图片高度 ); } else { + print('📁 [VideoItem] 渲染本地缩略图'); + // 本地文件 final file = File(_thumbnailPath!); if (file.existsSync()) { + print('✅ [VideoItem] 本地缩略图文件存在,开始渲染'); return Image.file( file, fit: BoxFit.cover, width: double.infinity, height: double.infinity, + // 🚀 设置缓存宽高,减少内存占用 + cacheWidth: 400, + cacheHeight: 300, + errorBuilder: (context, error, stackTrace) { + print('❌ [VideoItem] 本地缩略图渲染失败: $error'); + return const SizedBox.shrink(); + }, ); } else { + print('❌ [VideoItem] 本地缩略图文件不存在: $_thumbnailPath'); return const SizedBox.shrink(); } } } + + // 🎯 构建视频第一帧预览(备选方案) + Widget _buildVideoPreview(String videoPath) { + return FutureBuilder( + future: _initVideoPreview(videoPath), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData && + snapshot.data!.value.isInitialized) { + print('✅ [VideoItem] 视频预览加载成功'); + // 🎯 使用 FittedBox 保持视频比例,避免变形 + return FittedBox( + fit: BoxFit.cover, + child: SizedBox( + width: snapshot.data!.value.size.width, + height: snapshot.data!.value.size.height, + child: VideoPlayer(snapshot.data!), + ), + ); + } else { + print('⏳ [VideoItem] 视频预览加载中...'); + return Container( + color: Colors.grey[300], + child: Center( + child: SizedBox( + width: 24.w, + height: 24.w, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.grey[600], + ), + ), + ), + ); + } + }, + ); + } + + // 初始化视频预览 + Future _initVideoPreview(String videoPath) async { + final controller = VideoPlayerController.file(File(videoPath)); + await controller.initialize(); + await controller.seekTo(Duration.zero); // 定位到第一帧 + return controller; + } } diff --git a/pubspec.lock b/pubspec.lock index 3389ecc..432779d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1570,6 +1570,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + video_thumbnail: + dependency: "direct main" + description: + name: video_thumbnail + sha256: "181a0c205b353918954a881f53a3441476b9e301641688a581e0c13f00dc588b" + url: "https://pub.dev" + source: hosted + version: "0.5.6" visibility_detector: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f38e319..db85418 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,6 +62,7 @@ dependencies: video_player: ^2.9.2 chewie: ^1.8.5 # 视频播放器UI audioplayers: ^6.5.1 + video_thumbnail: ^0.5.3 # 视频缩略图生成 fluwx: ^5.7.5 tobias: ^5.3.1 agora_rtc_engine: ^6.5.3