From 2f6292d672758e8442f729d1ab1d5946ece2eb03 Mon Sep 17 00:00:00 2001 From: Junker Date: Mon, 30 Dec 2024 23:57:53 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20=E5=8D=8E=E4=B8=BAp30?= =?UTF-8?q?=E3=80=81=E5=8D=8E=E4=B8=BAp30=20pro=20=E5=BD=95=E5=88=B6?= =?UTF-8?q?=E5=92=8C=E6=92=AD=E6=94=BE=E8=A7=86=E9=A2=91=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 813fa2bb4669b5b5773bf07b510d2d4a8b4ebfaf) --- .../TIMUIKitTextField/intl_camer_picker.dart | 105 -------- .../tim_uikit_more_panel.dart | 227 +++++++++--------- pubspec.yaml | 4 +- 3 files changed, 110 insertions(+), 226 deletions(-) delete mode 100644 lib/ui/views/TIMUIKitChat/TIMUIKitTextField/intl_camer_picker.dart diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/intl_camer_picker.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/intl_camer_picker.dart deleted file mode 100644 index d876555a..00000000 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/intl_camer_picker.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'package:tencent_im_base/tencent_im_base.dart'; -import 'package:wechat_camera_picker/wechat_camera_picker.dart'; - -class IntlCameraPickerTextDelegate extends CameraPickerTextDelegate { - /// Confirm string for the confirm button. - /// 确认按钮的字段 - @override - String get confirm => TIM_t('确认'); - - /// Tips string above the shooting button before shooting. - /// 拍摄前确认按钮上方的提示文字 - @override - String get shootingTips => TIM_t('轻触拍照,长按摄像'); - - /// Tips string above the shooting button before shooting. - /// 拍摄前确认按钮上方的提示文字 - @override - String get shootingWithRecordingTips => TIM_t('轻触拍照,长按摄像'); - - /// Load failed string for item. - /// 资源加载失败时的字段 - @override - String get loadFailed => TIM_t('加载失败'); - - /// Default loading string for the dialog. - /// 加载中弹窗的默认文字 - @override - String get loading => TIM_t('加载中…'); - - /// Saving string for the dialog. - /// 保存中弹窗的默认文字 - @override - String get saving => TIM_t('保存中…'); - - /// Semantics fields. - /// - /// Fields below are only for semantics usage. For customizable these fields, - /// head over to [EnglishCameraPickerTextDelegate] for better understanding. - @override - String get sActionManuallyFocusHint => TIM_t('手动聚焦'); - - @override - String get sActionPreviewHint => TIM_t('预览'); - - @override - String get sActionRecordHint => TIM_t('录像'); - - @override - String get sActionShootHint => TIM_t('拍照'); - - @override - String get sActionShootingButtonTooltip => TIM_t('拍照按钮'); - - @override - String get sActionStopRecordingHint => TIM_t('停止录像'); - - @override - String sCameraLensDirectionLabel(CameraLensDirection value) { - switch (value) { - case CameraLensDirection.front: - return TIM_t('前置'); - case CameraLensDirection.back: - return TIM_t('后置'); - case CameraLensDirection.external: - return TIM_t('外置'); - } - } - - @override - String? sCameraPreviewLabel(CameraLensDirection? value) { - if (value == null) { - return null; - } - final option1 = sCameraLensDirectionLabel(value); - return TIM_t_para("{{option1}} 画面预览", "$option1 画面预览")(option1: option1); - } - - @override - String sFlashModeLabel(FlashMode mode) { - final String _modeString; - switch (mode) { - case FlashMode.off: - _modeString = TIM_t('关闭'); - break; - case FlashMode.auto: - _modeString = TIM_t('自动'); - break; - case FlashMode.always: - _modeString = TIM_t('拍照时闪光'); - break; - case FlashMode.torch: - _modeString = TIM_t('始终闪光'); - break; - } - final option2 = _modeString; - return TIM_t_para("闪光模式: {{option2}}", "闪光模式: $option2")(option2: option2); - } - - @override - String sSwitchCameraLensDirectionLabel(CameraLensDirection value) { - final option3 = sCameraLensDirectionLabel(value); - return TIM_t_para("切换至 {{option3}} 摄像头", "切换至 $option3 摄像头")( - option3: option3); - } -} diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_more_panel.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_more_panel.dart index ae8cbaa4..01770da5 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_more_panel.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_more_panel.dart @@ -12,7 +12,6 @@ import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_call_invite_list.dart'; -import 'package:wechat_camera_picker/wechat_camera_picker.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/separate_models/tui_chat_separate_view_model.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; @@ -22,7 +21,7 @@ import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/message.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/permission.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/platform.dart'; -import 'package:tencent_cloud_chat_uikit/ui/views/TIMUIKitChat/TIMUIKitTextField/intl_camer_picker.dart'; +import 'package:video_player/video_player.dart'; import 'package:wechat_assets_picker/wechat_assets_picker.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; @@ -65,8 +64,7 @@ class MorePanelItem { final Widget icon; final Function(BuildContext context)? onTap; - MorePanelItem( - {this.onTap, required this.icon, required this.id, required this.title}); + MorePanelItem({this.onTap, required this.icon, required this.id, required this.title}); } class MorePanel extends StatefulWidget { @@ -91,8 +89,7 @@ class MorePanel extends StatefulWidget { class _MorePanelState extends TIMUIKitState { final ImagePicker _picker = ImagePicker(); - final TUISelfInfoViewModel _selfInfoViewModel = - serviceLocator(); + final TUISelfInfoViewModel _selfInfoViewModel = serviceLocator(); Uint8List? fileContent; String? fileName; File? tempFile; @@ -102,6 +99,8 @@ class _MorePanelState extends TIMUIKitState { final ScrollController _scrollController = ScrollController(); final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + VideoPlayerController? _videoPlayerController; + @override void initState() { super.initState(); @@ -119,18 +118,37 @@ class _MorePanelState extends TIMUIKitState { return [ if (PlatformUtils().isMobile) MorePanelItem( - id: "screen", - title: TIM_t("拍摄"), + id: "take_photo", + title: TIM_t("拍照"), + onTap: (c) { + _onFeatureTap("take_photo", c, model, theme); + }, + icon: Container( + height: 64, + width: 64, + margin: const EdgeInsets.only(bottom: 4), + decoration: const BoxDecoration( + color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))), + child: SvgPicture.asset( + "images/screen.svg", + package: 'tencent_cloud_chat_uikit', + height: 64, + width: 64, + ), + )), + if (PlatformUtils().isMobile) + MorePanelItem( + id: "video_tape", + title: TIM_t("录像"), onTap: (c) { - _onFeatureTap("screen", c, model, theme); + _onFeatureTap("video_tape", c, model, theme); }, icon: Container( height: 64, width: 64, margin: const EdgeInsets.only(bottom: 4), decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(5))), + color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))), child: SvgPicture.asset( "images/screen.svg", package: 'tencent_cloud_chat_uikit', @@ -155,8 +173,7 @@ class _MorePanelState extends TIMUIKitState { width: 64, margin: const EdgeInsets.only(bottom: 4), decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(5))), + color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))), child: SvgPicture.asset( "images/photo.svg", package: 'tencent_cloud_chat_uikit', @@ -181,8 +198,7 @@ class _MorePanelState extends TIMUIKitState { width: 64, margin: const EdgeInsets.only(bottom: 4), decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(5))), + color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))), child: SvgPicture.asset( "images/photo.svg", package: 'tencent_cloud_chat_uikit', @@ -207,10 +223,8 @@ class _MorePanelState extends TIMUIKitState { width: 64, margin: const EdgeInsets.only(bottom: 4), decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(5))), - child: - Icon(Icons.video_file, color: hexToColor("5c6168"), size: 26), + color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))), + child: Icon(Icons.video_file, color: hexToColor("5c6168"), size: 26), )), MorePanelItem( id: "file", @@ -228,8 +242,7 @@ class _MorePanelState extends TIMUIKitState { width: 64, margin: const EdgeInsets.only(bottom: 4), decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(5))), + color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))), child: SvgPicture.asset( "images/file.svg", package: 'tencent_cloud_chat_uikit', @@ -254,8 +267,7 @@ class _MorePanelState extends TIMUIKitState { width: 64, margin: const EdgeInsets.only(bottom: 4), decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(5))), + color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))), child: SvgPicture.asset( "images/video-call.svg", package: 'tencent_cloud_chat_uikit', @@ -280,8 +292,7 @@ class _MorePanelState extends TIMUIKitState { width: 64, margin: const EdgeInsets.only(bottom: 4), decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(5))), + color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))), child: SvgPicture.asset( "images/voice-call.svg", package: 'tencent_cloud_chat_uikit', @@ -291,7 +302,11 @@ class _MorePanelState extends TIMUIKitState { )), if (config.extraAction != null) ...?config.extraAction, ].where((element) { - if (element.id == "screen") { + if (element.id == "take_photo") { + return config.showCameraAction; + } + + if (element.id == "video_tape") { return config.showCameraAction; } @@ -320,28 +335,22 @@ class _MorePanelState extends TIMUIKitState { }).toList(); } - _sendVideoMessage(AssetEntity asset, int size, TUIChatSeparateViewModel model) async { + _sendVideoMessage( + String originFilePath, int duration, int size, TUIChatSeparateViewModel model) async { if (size >= MorePanelConfig.VIDEO_MAX_SIZE) { - onTIMCallback(TIMCallback( - type: TIMCallbackType.INFO, - infoRecommendText: TIM_t("文件大小超出了限制"))); + onTIMCallback(TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("文件大小超出了限制"))); return; } final plugin = FcNativeVideoThumbnail(); - final originFile = await asset.originFile; - final duration = asset.videoDuration.inSeconds; - final filePath = originFile!.path; final convID = widget.conversationID; final convType = widget.conversationType; - String tempPath = (await getTemporaryDirectory()).path + - p.basename(originFile.path) + - ".jpeg"; + String tempPath = (await getTemporaryDirectory()).path + p.basename(originFilePath) + ".jpeg"; await plugin.getVideoThumbnail( - srcFile: originFile.path, + srcFile: originFilePath, destFile: tempPath, format: 'jpeg', width: 1280, @@ -350,12 +359,14 @@ class _MorePanelState extends TIMUIKitState { ); MessageUtils.handleMessageError( model.sendVideoMessage( - videoPath: filePath, + videoPath: originFilePath, duration: duration, snapshotPath: tempPath, convID: convID, convType: convType), context); + // 释放资源 + _videoPlayerController?.dispose(); } _sendImageMessage(TUIChatSeparateViewModel model, TUITheme theme) async { @@ -415,44 +426,38 @@ class _MorePanelState extends TIMUIKitState { if (type == AssetType.image) { if (size >= MorePanelConfig.IMAGE_MAX_SIZE) { onTIMCallback(TIMCallback( - type: TIMCallbackType.INFO, - infoRecommendText: TIM_t("文件大小超出了限制"))); + type: TIMCallbackType.INFO, infoRecommendText: TIM_t("文件大小超出了限制"))); return; } MessageUtils.handleMessageError( - model.sendImageMessage( - imagePath: filePath, - convID: convID, - convType: convType), + model.sendImageMessage(imagePath: filePath, convID: convID, convType: convType), context); } if (type == AssetType.video) { - _sendVideoMessage(asset, size, model); + final originFile = await asset.originFile; + _sendVideoMessage(originFile!.path, asset.videoDuration.inSeconds, size, model); } } } } } else { - FilePickerResult? result = - await FilePicker.platform.pickFiles(type: FileType.media); + FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.media); if (result != null && result.files.isNotEmpty) { File file = File(result.files.single.path!); final String savePath = file.path; - final String type = TencentUtils.getFileType( - savePath.split(".")[savePath.split(".").length - 1]) - .split("/")[0]; + final String type = + TencentUtils.getFileType(savePath.split(".")[savePath.split(".").length - 1]) + .split("/")[0]; if (type == "image") { MessageUtils.handleMessageError( - model.sendImageMessage( - imagePath: savePath, convID: convID, convType: convType), + model.sendImageMessage(imagePath: savePath, convID: convID, convType: convType), context); } else if (type == "video") { MessageUtils.handleMessageError( - model.sendVideoMessage( - videoPath: savePath, convID: convID, convType: convType), + model.sendVideoMessage(videoPath: savePath, convID: convID, convType: convType), context); } } else { @@ -466,8 +471,9 @@ class _MorePanelState extends TIMUIKitState { _sendImageFromCamera( TUIChatSeparateViewModel model, - TUITheme theme, - ) async { + TUITheme theme, { + required bool isVideo, + }) async { try { if (!await Permissions.checkPermission( context, @@ -484,34 +490,30 @@ class _MorePanelState extends TIMUIKitState { final convID = widget.conversationID; final convType = widget.conversationType; - final pickedFile = await CameraPicker.pickFromCamera(context, - pickerConfig: CameraPickerConfig( - enableRecording: true, - textDelegate: IntlCameraPickerTextDelegate())); - final originFile = await pickedFile?.originFile; - if (originFile != null) { - final type = pickedFile!.type; - final size = await originFile!.length(); - if (type == AssetType.image) { - if (size >= MorePanelConfig.IMAGE_MAX_SIZE) { - onTIMCallback(TIMCallback( - type: TIMCallbackType.INFO, - infoRecommendText: TIM_t("文件大小超出了限制"))); - return; - } - - MessageUtils.handleMessageError( - model.sendImageMessage( - imagePath: originFile.path, - convID: convID, - convType: convType), - context); - } - if (type == AssetType.video) { - _sendVideoMessage(pickedFile, size, model); + final ImagePicker picker = ImagePicker(); + XFile? originFile; + if (isVideo) { + originFile = await picker.pickVideo(source: ImageSource.camera); + } else { + originFile = await picker.pickImage(source: ImageSource.camera); + } + final size = await originFile!.length(); + if (!isVideo) { + if (size >= MorePanelConfig.IMAGE_MAX_SIZE) { + onTIMCallback( + TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("文件大小超出了限制"))); + return; } + + MessageUtils.handleMessageError( + model.sendImageMessage(imagePath: originFile.path, convID: convID, convType: convType), + context); } else { - // Toast.showToast(ToastType.fail, TIM_t("图片不能为空"), context); + _videoPlayerController = VideoPlayerController.file(File(originFile.path)) + ..initialize().then((_) { + _sendVideoMessage(originFile!.path, + _videoPlayerController?.value.duration.inSeconds ?? 10, size, model); + }); } } catch (error) { outputLogger.i("err: $error"); @@ -527,9 +529,8 @@ class _MorePanelState extends TIMUIKitState { fileContent = imageContent; html.Node? inputElem; - inputElem = html.document - .getElementById("__image_picker_web-file-input") - ?.querySelector("input"); + inputElem = + html.document.getElementById("__image_picker_web-file-input")?.querySelector("input"); final convID = widget.conversationID; final convType = widget.conversationType; MessageUtils.handleMessageError( @@ -561,9 +562,8 @@ class _MorePanelState extends TIMUIKitState { } html.Node? inputElem; - inputElem = html.document - .getElementById("__image_picker_web-file-input") - ?.querySelector("input"); + inputElem = + html.document.getElementById("__image_picker_web-file-input")?.querySelector("input"); final convID = widget.conversationID; final convType = widget.conversationType; MessageUtils.handleMessageError( @@ -589,31 +589,25 @@ class _MorePanelState extends TIMUIKitState { if (result != null && result.files.isNotEmpty) { if (PlatformUtils().isWeb) { html.Node? inputElem; - inputElem = html.document - .getElementById("__file_picker_web-file-input") - ?.querySelector("input"); + inputElem = + html.document.getElementById("__file_picker_web-file-input")?.querySelector("input"); fileName = result.files.single.name; MessageUtils.handleMessageError( model.sendFileMessage( - inputElement: inputElem, - fileName: fileName, - convID: convID, - convType: convType), + inputElement: inputElem, fileName: fileName, convID: convID, convType: convType), context); return; } String? option2 = result.files.single.path ?? ""; - outputLogger - .i(TIM_t_para("选择成功{{option2}}", "选择成功$option2")(option2: option2)); + outputLogger.i(TIM_t_para("选择成功{{option2}}", "选择成功$option2")(option2: option2)); File file = File(result.files.single.path!); final int size = file.lengthSync(); if (size >= MorePanelConfig.FILE_MAX_SIZE) { - onTIMCallback(TIMCallback( - type: TIMCallbackType.INFO, - infoRecommendText: TIM_t("文件大小超出了限制"))); + onTIMCallback( + TIMCallback(type: TIMCallbackType.INFO, infoRecommendText: TIM_t("文件大小超出了限制"))); return; } @@ -621,10 +615,7 @@ class _MorePanelState extends TIMUIKitState { MessageUtils.handleMessageError( model.sendFileMessage( - filePath: savePath, - size: size, - convID: convID, - convType: convType), + filePath: savePath, size: size, convID: convID, convType: convType), context); } else { throw TypeError(); @@ -644,8 +635,11 @@ class _MorePanelState extends TIMUIKitState { case "photo": _sendImageMessage(model, theme); break; - case "screen": - _sendImageFromCamera(model, theme); + case "take_photo": + _sendImageFromCamera(model, theme, isVideo: false); + break; + case "video_tape": + _sendImageFromCamera(model, theme, isVideo: true); break; case "file": _sendFile(model, theme); @@ -673,14 +667,14 @@ class _MorePanelState extends TIMUIKitState { bool hasMicrophonePermission = false; if (type == TYPE_VIDEO) { hasCameraPermission = await Permissions.checkPermission(context, Permission.camera.value); - hasMicrophonePermission = await Permissions.checkPermission( - context, Permission.microphone.value); + hasMicrophonePermission = + await Permissions.checkPermission(context, Permission.microphone.value); if (!hasCameraPermission || !hasMicrophonePermission) { return; } } else { - hasMicrophonePermission = await Permissions.checkPermission( - context, Permission.microphone.value); + hasMicrophonePermission = + await Permissions.checkPermission(context, Permission.microphone.value); if (!hasMicrophonePermission) { return; } @@ -702,9 +696,7 @@ class _MorePanelState extends TIMUIKitState { _tUICore.callService(TUICALLKIT_SERVICE_NAME, METHOD_NAME_CALL, { PARAM_NAME_TYPE: type, PARAM_NAME_USERIDS: inviteMember, - PARAM_NAME_GROUPID: widget.conversationType == ConvType.group - ? widget.conversationID - : "" + PARAM_NAME_GROUPID: widget.conversationType == ConvType.group ? widget.conversationID : "" }); } } else { @@ -719,8 +711,7 @@ class _MorePanelState extends TIMUIKitState { @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; - final TUIChatSeparateViewModel model = - Provider.of(context); + final TUIChatSeparateViewModel model = Provider.of(context); final screenWidth = MediaQuery.of(context).size.width; return Container( height: 248, @@ -758,14 +749,12 @@ class _MorePanelState extends TIMUIKitState { width: 64, margin: const EdgeInsets.only(bottom: 4), decoration: const BoxDecoration( - borderRadius: - BorderRadius.all(Radius.circular(5))), + borderRadius: BorderRadius.all(Radius.circular(5))), child: item.icon, ), Text( item.title, - style: TextStyle( - fontSize: 12, color: theme.darkTextColor), + style: TextStyle(fontSize: 12, color: theme.darkTextColor), ) ], ), diff --git a/pubspec.yaml b/pubspec.yaml index 76827921..98b99cee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: get_it: ^7.2.0 dotted_border: ^2.0.0+2 flutter_svg: ^2.0.6 - image_picker: ^0.8.5+3 + image_picker: ^0.8.9 file_picker: ^5.3.0 tencent_super_tooltip: ^0.0.1 video_player: ^2.9.0 @@ -41,7 +41,7 @@ dependencies: shared_preferences: ^2.0.13 scroll_to_index: ^2.1.1 wechat_assets_picker: ^9.3.3 - wechat_camera_picker: ^4.3.4 +# wechat_camera_picker: ^4.3.4 flutter_easyrefresh: ^2.2.1 extended_image: ^9.0.0 extended_text_field: ^16.0.0 From 79052368a6ade7c5793515a2ad0fac6b1c427575 Mon Sep 17 00:00:00 2001 From: Junker Date: Wed, 1 Jan 2025 23:49:14 +0800 Subject: [PATCH 2/2] =?UTF-8?q?1=E3=80=81=E4=BC=98=E5=8C=96=20=E5=8D=8E?= =?UTF-8?q?=E4=B8=BAp30=E3=80=81=E5=8D=8E=E4=B8=BAp30=20pro=20=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E5=BD=95=E5=88=B6=E5=8A=9F=E8=83=BD=202=E3=80=81?= =?UTF-8?q?=E8=A7=A3=E5=86=B3=20=E5=8D=8E=E4=B8=BAp30=E3=80=81=E5=8D=8E?= =?UTF-8?q?=E4=B8=BAp30=20pro=20=E6=97=A0=E6=B3=95=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- images/video_tape.png | Bin 0 -> 2802 bytes ...encent_cloud_chat_message_videoplayer.dart | 197 ++++++++++++++++++ .../tim_uikit_more_panel.dart | 34 ++- lib/ui/widgets/video_screen.dart | 41 +--- pubspec.yaml | 3 +- 5 files changed, 230 insertions(+), 45 deletions(-) create mode 100644 images/video_tape.png create mode 100644 lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tencent_cloud_chat_message_viewer/tencent_cloud_chat_message_videoplayer.dart diff --git a/images/video_tape.png b/images/video_tape.png new file mode 100644 index 0000000000000000000000000000000000000000..f9714116aa60906e64090960050470e49d0926dc GIT binary patch literal 2802 zcmd^9c{tQx7r$Rq(-0v`m=ThQ-YjV$Wr>lpFGU-#Eg~c`NsQgtYi7zamVT5iWgFQ| zii|ZeOjMR>ifq}38T@#c|K2~|-yiQ^?|Gj4oO?dcz4x4R&*vPRxv3!^j|2|@fY0c> zz9j$90yF z*4Nj`RkhCUKDT`_^v^?>pfF0)`=zC&xw-kL=~$DJ=1ivkPCAjE*tN$yJW7x5J|o>Kfj<-13>8nTby#G`CRa=NAZ1Ntw@{ z56}nwFu`kUYk395--bsyob{42GL!i;G%_|gEV{b3!P(7wW@h$vDLFTM_UKnYzi9M4D)|6` zbA3YTEpHtA99QVNc%uOaL0ovzHSZw0%w5bYf7ZBGXcE z(8gm4y&-?A&%XwN7)l-i9@UAz*;+^NB~ z%vGYj8O8Im-#E2DFtXzW{Pc^Pp2?l}K1|BvnO`(%pID*NJqkmG3t zM|&bj$GEbUV_=9Z^<43kb80j|DL;8Uug9A%ch1PpOTgWw1dnoxL<}8QGP>d|==By| zCW>HkYlDhKfu^G+$rWJ<4tvNivGnYR9cwknd3C<9Os)NO2vyuPG$Zo?aULju!^t@4Tlul?jr8#ig6- z$M>;(spb0Smxc2izBdwiM3gorUfSf_2CzI{g*B5JMHOw2uo$KmRYscm0|cq?)IWU6 zq2X5|@PQ!YVP165M7#)`u_vphEXFI*?e+ewwX&GAQW9!7`0R|!-jzBJ>cJ~@9_k7Qqe+VE|&QE^8|e>{7WbMDze1A~dDVv8}=I59!uoe_3z`;08uD_?OQffR!~&YVMXa zgMiPN^VA}N3w7GR1~tA|;B*}7A|+tjpe|yhDGhciWYke>3FNiwUe*_03EuAsy0KD-U#%Tc%Fl&$tfvQdE1AKt;&M--qA5ie*?E{lYK^y=l zWJR_<2onf-wlHDz#6%`QoxVJ)bBIfa_U};tdri?6z&`bbKpm%zvE|`veW1qH>l*We zlUtcqUJ2QDe#yIm$OE$L)=l>VZ1K1H%xz;Oc-wIGC;*_Xm_Kd%?n9P^BEZhi$p?G_ zz;kKhnKb~+^6rzp=mv=XD^8j5v*7|4Fz*UkH_<<40)Z0Ro8gBce^?`Q^!^zz7B{$> zdYC_ZA8`^&DMAt_xl)RRh?5|t=uCVe-w^nbj;6tPQuG-4^r|Cp2X)*n6EwJlh~KKR z1yoWvp`xfxdcx)4KW+DBzR#_e+$uzy+o*=MdGqfVMqY#|fUHup zuiub;iR4ne?{5;D;AF;-id%SCWi~f{cBa^>&aQFSON%-?Sjo9LR}l(N{UZ_3zKDJT z9qB>ZabdBU=dos$<2cVz{MEn%=`82)%vSi>xSrj((KvCHEH@Bg2EZ+6Zw?nN)#* z3%%S%P7x@{WE7ez>YgE7E>@s3nTze2hyI3_PEx%faz&1~w?(m6@vu`JIziNETG`mV z^P>dSTGgUn?_#dy2jR2|{j?Nxj)77xjbc47c||%y>qXy;Mf@aLl{|xWq-V#8FDfgb zuP>_NM@5>mi)38=?i;B7__g;K@`gEYwV30cb*MQ;SL*J>vI2auCdI1uoSki6LN)U$ zVJCUsR)6=2PVPf`6*5Gg}D;Ax{Sm2##8sjl5oPi z@23zt@4z3oH&ysWSv@wE5RIS;=SdehBTnz_xlGczVIWlU(=#N)sda5bd`k1I1{2{M z=*!FAIIA@#`F&g^LiQwk7bg-mHB5QVVs;&EeEc<|{S;}pY>!xEsoTK_W#pTy(B_A* z;vqg(WyO~-c2|FHcB~DW-o4MgDYi1BjlpcNI)?(f?dyV?H}fwm+iQiaEO#;%qWNcr b!^NdQ#j4uiX4|#G?S?TjFx7vh=MeTM|C6CS literal 0 HcmV?d00001 diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tencent_cloud_chat_message_viewer/tencent_cloud_chat_message_videoplayer.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tencent_cloud_chat_message_viewer/tencent_cloud_chat_message_videoplayer.dart new file mode 100644 index 00000000..400988f6 --- /dev/null +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitMessageItem/tencent_cloud_chat_message_viewer/tencent_cloud_chat_message_videoplayer.dart @@ -0,0 +1,197 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:better_player_plus/better_player_plus.dart'; +import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; + +class TencentCloudChatMessageVideoPlayer extends StatefulWidget { + final V2TimMessage message; + final bool controller; + final bool isSending; + + const TencentCloudChatMessageVideoPlayer({ + super.key, + required this.message, + required this.controller, + required this.isSending, + }); + + @override + State createState() => TencentCloudChatMessageVideoPlayerState(); +} + +enum CurrentVideoType { + online, + local, +} + +class CurrentVideoInfo { + final String path; + final CurrentVideoType type; + final double aspectRatio; + + CurrentVideoInfo({ + required this.path, + required this.type, + required this.aspectRatio, + }); +} + +class TencentCloudChatMessageVideoPlayerState extends State { + final String _tag = "TencentCloudChatMessageVideoPlayer"; + + BetterPlayerController? _betterPlayerController; + + @override + void initState() { + super.initState(); + _initializePlayer(); + } + + Future _initializePlayer() async { + try { + final info = await getMessageInfo(); + if (info != null && mounted) { + BetterPlayerDataSource dataSource; + if (info.type == CurrentVideoType.online) { + dataSource = BetterPlayerDataSource( + BetterPlayerDataSourceType.network, + info.path, + ); + } else { + dataSource = BetterPlayerDataSource( + BetterPlayerDataSourceType.file, + info.path, + ); + } + + final betterPlayerConfiguration = BetterPlayerConfiguration( + aspectRatio: info.aspectRatio, + fit: BoxFit.contain, + autoPlay: true, + allowedScreenSleep: false, + fullScreenByDefault: false, + controlsConfiguration: const BetterPlayerControlsConfiguration( + enableFullscreen: false, + enablePlayPause: true, + enableProgressBar: true, + enableProgressText: true, + showControlsOnInitialize: false, + enableMute:false, + enableOverflowMenu:false, + enableSkips:false, + ), + ); + + _betterPlayerController = BetterPlayerController( + betterPlayerConfiguration, + betterPlayerDataSource: dataSource, + ); + + if (mounted) { + setState(() { + }); + } + } + } catch (e) { + debugPrint("Video initialization error: $e"); + } + } + + @override + void dispose() { + _betterPlayerController?.dispose(); + super.dispose(); + } + + Future getMessageInfo() async { + if (widget.message.elemType == MessageElemType.V2TIM_ELEM_TYPE_VIDEO) { + double aspectRatio = (9 / 16); + + if (widget.isSending) { + var lp = widget.message.videoElem!.videoPath ?? ""; + if (lp.isNotEmpty) { + console("view sending message video path"); + if (File(lp).existsSync() && !kIsWeb) { + return CurrentVideoInfo(path: lp, type: CurrentVideoType.local, aspectRatio: aspectRatio); + } + } + } + + if (widget.message.videoElem!.snapshotWidth != null && widget.message.videoElem!.snapshotHeight != null) { + if (widget.message.videoElem!.snapshotHeight != 0) { + aspectRatio = (widget.message.videoElem!.snapshotWidth!) / (widget.message.videoElem!.snapshotHeight!); + } + } + + if (TencentUtils.checkString(widget.message.videoElem!.videoPath) != null) { + // 先查本地发送的视频地址 + if (File(widget.message.videoElem!.videoPath!).existsSync()) { + console("video: local video path exists"); + return CurrentVideoInfo(path: widget.message.videoElem!.videoPath!, type: CurrentVideoType.local, aspectRatio: aspectRatio); + } + } else if (TencentUtils.checkString(widget.message.videoElem!.localVideoUrl) != null) { + // 再查本地下载的视频地址 + if (File(widget.message.videoElem!.localVideoUrl!).existsSync()) { + console("video: local url exists"); + return CurrentVideoInfo(path: widget.message.videoElem!.localVideoUrl!, type: CurrentVideoType.local, aspectRatio: aspectRatio); + } + } else { + // 最后再查在线地址(todo 使用 getMessageOnlineUrl 查询) + if (widget.message.videoElem != null) { + if (widget.message.videoElem!.snapshotUrl != null) { + console("video: online url ${widget.message.videoElem!.videoUrl}"); + return CurrentVideoInfo( + path: widget.message.videoElem!.videoUrl!, + type: CurrentVideoType.online, + aspectRatio: aspectRatio, + ); + } + } + if (!kIsWeb) { + V2TimValueCallback urlres = await TencentImSDKPlugin.v2TIMManager.getMessageManager().getMessageOnlineUrl(msgID: widget.message.msgID ?? ""); + if (urlres.data != null) { + if (urlres.data?.videoElem != null) { + if (TencentUtils.checkString(urlres.data?.videoElem?.videoUrl) != null) { + console("view video online url ${urlres.data?.videoElem?.videoUrl}"); + return CurrentVideoInfo(path: urlres.data!.videoElem!.videoUrl!, type: CurrentVideoType.online, aspectRatio: aspectRatio); + } + } + } + } + } + } else { + console("The component received a non-video message parameter. please check"); + } + console("has no view video source. please check"); + return null; + } + + console(String log) { + print("$_tag, $log"); + } + + @override + Widget build(BuildContext context) { + if (widget.message.hasRiskContent == true) { + return const Center( + child: Text( + "Risk Video", + style: TextStyle(color: Colors.white), + ), + ); + } + + if (_betterPlayerController == null) { + return Container(); + } + + return AspectRatio( + aspectRatio: _betterPlayerController!.videoPlayerController?.value.aspectRatio ?? 9 / 16, + child: BetterPlayer( + controller: _betterPlayerController!, + ), + ); + } +} diff --git a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_more_panel.dart b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_more_panel.dart index 01770da5..06bcd0e2 100644 --- a/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_more_panel.dart +++ b/lib/ui/views/TIMUIKitChat/TIMUIKitTextField/tim_uikit_more_panel.dart @@ -1,6 +1,7 @@ // ignore_for_file: unused_field, avoid_print, unused_import import 'dart:io'; +import 'package:better_player_plus/better_player_plus.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:fc_native_video_thumbnail/fc_native_video_thumbnail.dart'; import 'package:flutter/foundation.dart'; @@ -8,6 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:image_picker/image_picker.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:just_audio/just_audio.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_uikit/tencent_cloud_chat_uikit.dart'; @@ -99,7 +101,8 @@ class _MorePanelState extends TIMUIKitState { final ScrollController _scrollController = ScrollController(); final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); - VideoPlayerController? _videoPlayerController; + // 创建AudioPlayer对象 + late BetterPlayerController _betterPlayerController; @override void initState() { @@ -110,6 +113,7 @@ class _MorePanelState extends TIMUIKitState { isInstallCallkit = value; }); }); + _betterPlayerController = BetterPlayerController(const BetterPlayerConfiguration()); } } @@ -149,8 +153,8 @@ class _MorePanelState extends TIMUIKitState { margin: const EdgeInsets.only(bottom: 4), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(5))), - child: SvgPicture.asset( - "images/screen.svg", + child: Image.asset( + "images/video_tape.png", package: 'tencent_cloud_chat_uikit', height: 64, width: 64, @@ -365,8 +369,9 @@ class _MorePanelState extends TIMUIKitState { convID: convID, convType: convType), context); + // 释放资源 - _videoPlayerController?.dispose(); + _betterPlayerController.dispose(); } _sendImageMessage(TUIChatSeparateViewModel model, TUITheme theme) async { @@ -509,11 +514,22 @@ class _MorePanelState extends TIMUIKitState { model.sendImageMessage(imagePath: originFile.path, convID: convID, convType: convType), context); } else { - _videoPlayerController = VideoPlayerController.file(File(originFile.path)) - ..initialize().then((_) { - _sendVideoMessage(originFile!.path, - _videoPlayerController?.value.duration.inSeconds ?? 10, size, model); - }); + // 监听视频准备完成事件 + _betterPlayerController.addEventsListener((event) { + if (event.betterPlayerEventType == BetterPlayerEventType.initialized) { + // 获取视频时长(单位:秒) + int durationInSeconds = _betterPlayerController.videoPlayerController?.value.duration?.inSeconds ?? 0; + _sendVideoMessage(originFile!.path, durationInSeconds, size, model); + } + }); + + // 加载视频源 + _betterPlayerController.setupDataSource( + BetterPlayerDataSource( + BetterPlayerDataSourceType.file, + originFile.path, // 替换为你的视频 URL + ), + ); } } catch (error) { outputLogger.i("err: $error"); diff --git a/lib/ui/widgets/video_screen.dart b/lib/ui/widgets/video_screen.dart index 6aa39cea..04ee2bf1 100644 --- a/lib/ui/widgets/video_screen.dart +++ b/lib/ui/widgets/video_screen.dart @@ -20,6 +20,8 @@ import 'package:tencent_cloud_chat_uikit/ui/widgets/video_custom_control.dart'; import 'package:universal_html/html.dart' as html; import 'package:video_player/video_player.dart'; +import '../views/TIMUIKitChat/TIMUIKitMessageItem/tencent_cloud_chat_message_viewer/tencent_cloud_chat_message_videoplayer.dart'; + class VideoScreen extends StatefulWidget { const VideoScreen({required this.message, required this.heroTag, required this.videoElement, Key? key}) : super(key: key); @@ -33,11 +35,8 @@ class VideoScreen extends StatefulWidget { } class _VideoScreenState extends TIMUIKitState { - late VideoPlayerController videoPlayerController; - late ChewieController chewieController; GlobalKey slidePagekey = GlobalKey(); final TUIChatGlobalModel model = serviceLocator(); - bool isInit = false; @override initState() { @@ -266,9 +265,6 @@ class _VideoScreenState extends TIMUIKitState { return await _saveVideo(); })); setState(() { - videoPlayerController = player; - chewieController = controller; - isInit = true; }); }); } @@ -287,10 +283,6 @@ class _VideoScreenState extends TIMUIKitState { SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, ]); - if (isInit) { - videoPlayerController.dispose(); - chewieController.dispose(); - } super.dispose(); } @@ -327,31 +319,10 @@ class _VideoScreenState extends TIMUIKitState { } return null; }, - child: ExtendedImageSlidePageHandler( - child: Container( - color: Colors.black, - child: isInit - ? Chewie( - controller: chewieController, - ) - : const Center(child: CircularProgressIndicator(color: Colors.white))), - heroBuilderForSlidingPage: (Widget result) { - return Hero( - tag: widget.heroTag, - child: result, - flightShuttleBuilder: (BuildContext flightContext, - Animation animation, - HeroFlightDirection flightDirection, - BuildContext fromHeroContext, - BuildContext toHeroContext) { - final Hero hero = (flightDirection == HeroFlightDirection.pop - ? fromHeroContext.widget - : toHeroContext.widget) as Hero; - - return hero.child; - }, - ); - }, + child: TencentCloudChatMessageVideoPlayer( + message: widget.message, + controller: true, + isSending: widget.message.status == MessageStatus.V2TIM_MSG_STATUS_SENDING, )), )); })); diff --git a/pubspec.yaml b/pubspec.yaml index 98b99cee..bc3202c0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,7 +28,8 @@ dependencies: image_picker: ^0.8.9 file_picker: ^5.3.0 tencent_super_tooltip: ^0.0.1 - video_player: ^2.9.0 + video_player: ^2.9.2 + better_player_plus: ^1.0.5 chewie: ^1.8.5 flutter_slidable_plus_plus: ^0.1.0 flutter_plugin_record_plus: ^0.0.19