import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:crypto/crypto.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:tencent_im_base/tencent_im_base.dart'; import 'package:extended_image/extended_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_chat_global_model.dart'; import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; import 'package:universal_html/html.dart' as html; import 'package:chewie_for_us/chewie_for_us.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.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/widgets/video_custom_control.dart'; import 'package:video_player/video_player.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; class VideoScreen extends StatefulWidget { const VideoScreen( {required this.message, required this.heroTag, required this.videoElement, Key? key}) : super(key: key); final V2TimMessage message; final dynamic heroTag; final V2TimVideoElem videoElement; @override State createState() => _VideoScreenState(); } class _VideoScreenState extends TIMUIKitState { late VideoPlayerController videoPlayerController; late ChewieController chewieController; GlobalKey slidePagekey = GlobalKey(); final TUIChatGlobalModel model = serviceLocator(); bool isInit = false; @override initState() { super.initState(); setVideoMessage(); // 允许横屏 SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, ]); } //保存网络视频到本地 Future _saveNetworkVideo( context, String videoUrl, { bool isAsset = true, }) async { if (PlatformUtils().isWeb) { RegExp exp = RegExp(r"((\.){1}[^?]{2,4})"); String? suffix = exp.allMatches(videoUrl).last.group(0); var xhr = html.HttpRequest(); xhr.open('get', videoUrl); xhr.responseType = 'arraybuffer'; xhr.onLoad.listen((event) { final a = html.AnchorElement( href: html.Url.createObjectUrl(html.Blob([xhr.response]))); a.download = '${md5.convert(utf8.encode(videoUrl)).toString()}$suffix'; a.click(); a.remove(); }); xhr.send(); return; } if(PlatformUtils().isMobile){ if (PlatformUtils().isIOS) { if (!await Permissions.checkPermission( context, Permission.photosAddOnly.value, )) { return; } } else { final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; if ((androidInfo.version.sdkInt) >= 33) { final videos = await Permissions.checkPermission( context,Permission.videos.value, ); if(!videos){ return; } } else { final storage = await Permissions.checkPermission( context, Permission.storage.value, ); if(!storage){ return; } } } } String savePath = videoUrl; if (!isAsset) { if (widget.message.msgID == null || widget.message.msgID!.isEmpty) { return; } if (model.getMessageProgress(widget.message.msgID) == 100) { String savePath; if (widget.message.videoElem!.localVideoUrl != null && widget.message.videoElem!.localVideoUrl != '') { savePath = widget.message.videoElem!.localVideoUrl!; } else { savePath = model.getFileMessageLocation(widget.message.msgID); } File f = File(savePath); if (f.existsSync()) { var result = await ImageGallerySaver.saveFile(savePath); if (PlatformUtils().isIOS) { if (result['isSuccess']) { onTIMCallback(TIMCallback( type: TIMCallbackType.INFO, infoRecommendText: TIM_t("视频保存成功"), infoCode: 6660402)); } else { onTIMCallback(TIMCallback( type: TIMCallbackType.INFO, infoRecommendText: TIM_t("视频保存失败"), infoCode: 6660403)); } } else { if (result != null) { onTIMCallback(TIMCallback( type: TIMCallbackType.INFO, infoRecommendText: TIM_t("视频保存成功"), infoCode: 6660402)); } else { onTIMCallback(TIMCallback( type: TIMCallbackType.INFO, infoRecommendText: TIM_t("视频保存失败"), infoCode: 6660403)); } } } } else { onTIMCallback(TIMCallback( type: TIMCallbackType.INFO, infoRecommendText: TIM_t("the message is downloading"), infoCode: -1)); } return; } var result = await ImageGallerySaver.saveFile(savePath); if (PlatformUtils().isIOS) { if (result['isSuccess']) { onTIMCallback(TIMCallback( type: TIMCallbackType.INFO, infoRecommendText: TIM_t("视频保存成功"), infoCode: 6660402)); } else { onTIMCallback(TIMCallback( type: TIMCallbackType.INFO, infoRecommendText: TIM_t("视频保存失败"), infoCode: 6660403)); } } else { if (result != null) { onTIMCallback(TIMCallback( type: TIMCallbackType.INFO, infoRecommendText: TIM_t("视频保存成功"), infoCode: 6660402)); } else { onTIMCallback(TIMCallback( type: TIMCallbackType.INFO, infoRecommendText: TIM_t("视频保存失败"), infoCode: 6660403)); } } return; } Future _saveVideo() async { if (PlatformUtils().isWeb) { return await _saveNetworkVideo( context, widget.videoElement.videoPath!, isAsset: true, ); } if (widget.videoElement.videoPath != '' && widget.videoElement.videoPath != null) { File f = File(widget.videoElement.videoPath!); if (f.existsSync()) { return await _saveNetworkVideo( context, widget.videoElement.videoPath!, isAsset: true, ); } } if (widget.videoElement.localVideoUrl != '' && widget.videoElement.localVideoUrl != null) { File f = File(widget.videoElement.localVideoUrl!); if (f.existsSync()) { return await _saveNetworkVideo( context, widget.videoElement.localVideoUrl!, isAsset: true, ); } } return await _saveNetworkVideo( context, widget.videoElement.videoUrl!, isAsset: false, ); } double getVideoHeight() { double height = widget.videoElement.snapshotHeight!.toDouble(); double width = widget.videoElement.snapshotWidth!.toDouble(); // 横图 if (width > height) { return height * 1.3; } return height; } double getVideoWidth() { double height = widget.videoElement.snapshotHeight!.toDouble(); double width = widget.videoElement.snapshotWidth!.toDouble(); // 横图 if (width > height) { return width * 1.3; } return width; } setVideoMessage() async { // Using local path while sending // VideoPlayerController player = widget.message.videoElem!.videoUrl == null || // widget.message.status == MessageStatus.V2TIM_MSG_STATUS_SENDING // ? VideoPlayerController.file(File( // widget.message.videoElem!.videoPath!, // )) // : (widget.message.videoElem?.localVideoUrl == null || // widget.message.videoElem?.localVideoUrl == "") // ? VideoPlayerController.network( // widget.message.videoElem!.videoUrl!, // ) // : VideoPlayerController.file(File( // widget.message.videoElem!.localVideoUrl!, // )); if (!PlatformUtils().isWeb) { if (widget.message.msgID != null || widget.message.msgID != '') { if (model.getMessageProgress(widget.message.msgID) == 100) { String savePath; if (widget.message.videoElem!.localVideoUrl != null && widget.message.videoElem!.localVideoUrl != '') { savePath = widget.message.videoElem!.localVideoUrl!; } else { savePath = model.getFileMessageLocation(widget.message.msgID); } File f = File(savePath); if (f.existsSync()) { widget.videoElement.localVideoUrl = model.getFileMessageLocation(widget.message.msgID); } } } } VideoPlayerController player = PlatformUtils().isWeb ? ((widget.videoElement.videoPath != null && widget.videoElement.videoPath!.isNotEmpty) || widget.message.status == MessageStatus.V2TIM_MSG_STATUS_SENDING ? VideoPlayerController.network( widget.videoElement.videoPath!, ) : (widget.videoElement.localVideoUrl == null || widget.videoElement.localVideoUrl == "") ? VideoPlayerController.network( widget.videoElement.videoUrl!, ) : VideoPlayerController.network( widget.videoElement.localVideoUrl!, )) : (widget.videoElement.videoPath != null && widget.videoElement.videoPath!.isNotEmpty) || widget.message.status == MessageStatus.V2TIM_MSG_STATUS_SENDING ? VideoPlayerController.file(File(widget.videoElement.videoPath!)) : (widget.videoElement.localVideoUrl == null || widget.videoElement.localVideoUrl == "") ? VideoPlayerController.network( widget.videoElement.videoUrl!, ) : VideoPlayerController.file(File( widget.videoElement.localVideoUrl!, )); await player.initialize(); WidgetsBinding.instance.addPostFrameCallback((_) { double w = getVideoWidth(); double h = getVideoHeight(); ChewieController controller = ChewieController( videoPlayerController: player, autoPlay: true, looping: false, showControlsOnInitialize: false, allowPlaybackSpeedChanging: false, aspectRatio: w == 0 || h == 0 ? null : w / h, customControls: VideoCustomControls(downloadFn: () async{ return await _saveVideo(); })); setState(() { videoPlayerController = player; chewieController = controller; isInit = true; }); }); } @override didUpdateWidget(oldWidget) { if (oldWidget.videoElement.videoUrl != widget.videoElement.videoUrl || oldWidget.videoElement.videoPath != widget.videoElement.videoPath) { setVideoMessage(); } super.didUpdateWidget(oldWidget); } @override void dispose() { SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, ]); if (isInit) { videoPlayerController.dispose(); chewieController.dispose(); } super.dispose(); } @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { return OrientationBuilder(builder: ((context, orientation) { return Scaffold( body: Container( color: Colors.transparent, constraints: BoxConstraints.expand( height: MediaQuery.of(context).size.height, ), child: ExtendedImageSlidePage( key: slidePagekey, slidePageBackgroundHandler: (Offset offset, Size size) { if (orientation == Orientation.landscape) { return Colors.black; } double opacity = 0.0; opacity = offset.distance / (Offset(size.width, size.height).distance / 2.0); return Colors.black .withOpacity(min(1.0, max(1.0 - opacity, 0.0))); }, slideType: SlideType.onlyImage, 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; }, ); }, )), )); })); } }