import 'dart:io'; import 'package:auto_route/auto_route.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:eitc_erm_dental_flutter/app_router.gr.dart'; import 'package:eitc_erm_dental_flutter/db_util.dart'; import 'package:eitc_erm_dental_flutter/entity/db/local_patient_info.dart'; import 'package:eitc_erm_dental_flutter/exts.dart'; import 'package:eitc_erm_dental_flutter/global.dart'; import 'package:eitc_erm_dental_flutter/pages/view/widget/video_patient_info_bar.dart'; import 'package:eitc_erm_dental_flutter/sp_util.dart'; import 'package:eitc_erm_dental_flutter/vm/global_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:just_audio/just_audio.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import '../../funcs.dart'; import '../../generated/assets.dart'; import 'widget/video_operation_view.dart'; ///视频查看页面 @RoutePage(name: "videoViewRoute") class VideoViewPage extends ConsumerStatefulWidget { const VideoViewPage({super.key}); @override ConsumerState createState() => _VideoViewPageState(); } class _VideoViewPageState extends ConsumerState with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { ///纹理ID int _textureId = -1; ///视频控制界面控制器 final VideoOperationViewController _operationViewController = VideoOperationViewController(); ///上一次拍照的路径 String _lastTakePhotoPath = ""; ///音频播放器 final AudioPlayer _takePhotoAudioPlayer = AudioPlayer(); ///是否已弹出 bool _hasPoped = false; ///已选择的咨询人信息 LocalPatientInfo? _patientInfo; ///wifi名称 String _wifiName = ""; ///设备型号 String _deviceModel = ""; ///当前牙齿区域 String _currentToothArea = ""; ///是否可以设备拍照 bool _canDeviceTakePhoto = true; ///是否正在选择牙齿区域 bool _isSelectingToothArea = false; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); //保持屏幕常亮 WakelockPlus.enable(); //开启屏幕旋转 screenEnableRotate(); //设置MethodChannel回调 videoChannel.setMethodCallHandler(_methodCallHandler); //音频播放器加载资源 _takePhotoAudioPlayer.setAsset(Assets.audiosTakePhoto); //开始视频 _startVideo(); //监听连接状态变化 ref.listenManual(deviceConnectStatusProvider(videoChannel), (_, bo) { if (!bo) { logd("连接丢失"); _popSelf(); } }); //初始化数据 _initData(); WidgetsBinding.instance.addPostFrameCallback((_) { //选择牙齿区域 _selectToothArea(); }); } ///初始化数据 void _initData() async { //初始化咨询人信息,设备型号 await Future.wait([_initPatientInfo(), _initDeviceModel()]); setState(() {}); } ///初始化咨询人信息 Future _initPatientInfo() async { if (selectedPatientId < 0) { return; } LocalPatientInfo? info = await DbUtil.instance.getLocalPatientById(selectedPatientId); if (info != null) { _patientInfo = info; } } ///初始化设备型号 Future _initDeviceModel() async { String? name = await getWifiName(); if (name.isNullOrEmpty) { return; } _wifiName = name!; _deviceModel = await getDeviceModel() ?? ""; logd("初始化设备型号,wifiName=$_wifiName,deviceModel=$_deviceModel"); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); //关闭屏幕常亮 WakelockPlus.disable(); if (!_hasPoped) { _stopRecord(); } _stopVideo(); videoChannel.setMethodCallHandler(null); //关闭屏幕旋转 screenDisableRotate(); _operationViewController.dispose(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (!(state == AppLifecycleState.inactive || state == AppLifecycleState.resumed)) { _popSelf(); } } @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { super.build(context); return Scaffold( body: SafeArea( child: Container( color: Colors.black, child: Stack( children: [ Center( child: _getVideo(), ), VideoOperationView( controller: _operationViewController, onTakePhoto: _takePhoto, onStartRecord: _startRecord, onStopRecord: () => _stopRecord(toPreview: true), onStopVideo: _stopVideo, area: _currentToothArea, ), Align( alignment: Alignment.topLeft, child: _getVideoViewToolBar(), ) ], ), )), ); } void _startVideo() async { if (!mounted) { return; } try { int result = await videoChannel.invokeMethod("startVideo"); setState(() { _textureId = result; }); } catch (e) { showToast(text: getS().showVideoError); loge("开始视频失败", error: e); } } void _stopVideo() async { logd("停止视频"); if (_textureId < 0) { return; } _textureId = -1; try { await videoChannel.invokeMethod("stopVideo"); } catch (e) { loge("停止视频失败", error: e); } } Future _methodCallHandler(MethodCall call) { logd(call); if (call.method == "onDeviceTakePhoto") { _onDeviceTakePhoto(call.arguments); } return Future.value(""); } ///获取视频Widget Widget _getVideo() { return OrientationBuilder(builder: (ctx, orientation) { if (Platform.isIOS) { Size size = MediaQuery.of(context).size; double width = size.width; double height = size.height; videoChannel.invokeMethod( orientation == Orientation.landscape ? "getLandscapeView" : "getPortraitView", [width, height]); logd("ios,width=$width,height=$height"); if (orientation == Orientation.landscape) { return AspectRatio( aspectRatio: 16.0 / 9.0, child: Stack( children: [ _getVideoView(true), Align( alignment: Alignment.bottomCenter, child: _getPatientInfoBar(orientation), ) ], ), ); } else { return Column( mainAxisSize: MainAxisSize.min, children: [ AspectRatio( aspectRatio: 16.0 / 9.0, child: _getVideoView(true), ), _getPatientInfoBar(orientation), ], ); } } else { if (orientation == Orientation.landscape) { return AspectRatio( aspectRatio: 16.0 / 9.0, child: Stack( children: [ _getVideoView(false), Align( alignment: Alignment.bottomCenter, child: _getPatientInfoBar(orientation), ) ], ), ); } else { return Column( mainAxisSize: MainAxisSize.min, children: [ AspectRatio( aspectRatio: 16.0 / 9.0, child: _getVideoView(false), ), _getPatientInfoBar(orientation) ], ); } } }); } Widget _getVideoView(bool isIos) { if (isIos) { return UiKitView( viewType: "VideoView", onPlatformViewCreated: (id) => logd("IOS PlatformView创建,id=$id"), ); } else { return _textureId >= 0 ? Texture(textureId: _textureId) : const SizedBox(); } } ///获取患者信息条 Widget _getPatientInfoBar(Orientation orientation) { return _operationViewController.isRecording ? SizedBox() : VideoPatientInfoBar( info: _patientInfo, orientation: orientation, area: _currentToothArea, deviceModel: _deviceModel); } ///获取工具栏 Widget _getVideoViewToolBar() { return Row( children: [ IconButton( onPressed: () => _popSelf(true), icon: Icon( Platform.isAndroid ? Icons.arrow_back : Icons.arrow_back_ios_new, color: Colors.white, )), const Spacer(), IconButton( onPressed: _gotoSettings, icon: const Icon( Icons.settings_outlined, color: Colors.white, )), ], ); } ///拍照 void _takePhoto() async { if (_operationViewController.isRecording) { return; } try { String path = await videoChannel.invokeMethod("takePhoto"); _showTakePhotoSuccess(path); } catch (e) { loge("拍照失败", error: e); showToast(text: getS().takePhotoFailed); } } ///当设备拍照 ///IOS平台设备和手机拍照都会调用 void _onDeviceTakePhoto(dynamic arguments) async { if (!_canDeviceTakePhoto || !isCurrentPage) { logd("设备拍照,但是不可拍照,返回"); return; } _takePhoto(); } void _showTakePhotoSuccess(String newPath) async { if (newPath == _lastTakePhotoPath) { return; } logd("拍照成功,path=$newPath"); _lastTakePhotoPath = newPath; showToast(text: getS().takePhotoSuccess); _playTakePhotoAudio(); String area = _currentToothArea; if (mounted) { setState(() { _currentToothArea = ""; _stopVideo(); }); await context.pushRoute(PhotoPreviewRoute( info: _patientInfo!, path: newPath, area: area, time: DateTime.now(), mobile: "", deviceModel: _deviceModel, wifi: _wifiName)); _selectToothArea(); _startVideo(); } } ///开始录像 void _startRecord() async { if (_operationViewController.isRecording) { return; } _canDeviceTakePhoto = false; try { await videoChannel.invokeMethod( "startRecord", makeFilePrefix( name: _patientInfo?.name ?? "", idCard: _patientInfo?.idCardDecrypt ?? "", mobile: "", area: _currentToothArea, wifi: _wifiName, time: DateTime.now().millisecondsSinceEpoch, userId: await SpUtil.getUserId())); _operationViewController.updateRecordingState(true); showToast(text: getS().hasStartRecord); setState(() {}); } catch (e) { _canDeviceTakePhoto = true; _operationViewController.updateRecordingState(false); loge("开始录像失败", error: e); showToast(text: getS().startRecordFailed); } } ///停止录像 void _stopRecord({bool toPreview = false}) async { if (!_operationViewController.isRecording) { return; } logd("停止录像"); _canDeviceTakePhoto = true; try { String path = await videoChannel.invokeMethod("stopRecord"); if (toPreview) { //显示loading延迟停止视频,因为停止录像是异步的,如果在没执行完就停止视频 //会导致录像文件丢失尾帧信息,播放时没有总时长 var cancel = BotToast.showLoading( crossPage: false, clickClose: false, backButtonBehavior: BackButtonBehavior.ignore); await Future.delayed(Duration(seconds: 1, milliseconds: 500)); cancel(); showToast(text: getS().hasStopRecord); setState(() { _operationViewController.updateRecordingState(false); _stopVideo(); }); if (mounted) { await context.pushRoute(VideoPreviewRoute( info: _patientInfo!, path: path, area: _currentToothArea, time: DateTime.now(), mobile: "", deviceModel: _deviceModel, wifi: _wifiName)); _selectToothArea(); _startVideo(); } } else { _operationViewController.updateRecordingState(false); showToast(text: getS().hasStopRecord); } } catch (e) { _operationViewController.updateRecordingState(false); loge("停止录像失败", error: e); showToast(text: getS().stopRecordFailed); } } ///播放拍照音效 void _playTakePhotoAudio() async { await _takePhotoAudioPlayer.seek(Duration.zero); await _takePhotoAudioPlayer.play(); } ///前往设置 void _gotoSettings() async { await context.pushRoute(const DelayShotSettingsRoute()); if (_hasPoped) { return; } screenEnableRotate(); } ///弹出自身页面 void _popSelf([bool force = false]) { if (force) { Navigator.pop(context); return; } if (_hasPoped) { return; } _hasPoped = true; _stopRecord(); //如果是锁屏导致的,dispose会在解锁后才会调用,导致在锁屏期间视频还是播放的,所以这里提前停止 _stopVideo(); //如果当前页面是最上层的就是pop,否则remove,否则会导致把后来打开的页面pop但是当前页面还存在的问题 if (isCurrentPage) { logd("视频查看页面pop自身"); //如果正在选择牙齿区域,先把弹窗pop掉 if (_isSelectingToothArea) { Navigator.pop(context); } Navigator.pop(context); } else { logd("视频查看页面remove自身"); AutoRouter.of(context).removeRoute(context.routeData); } } ///是否是当前页面 bool get isCurrentPage => AutoRouter.of(context).current.name == VideoViewRoute.name; ///选择延迟区域 void _selectToothArea() async { if (!mounted) { return; } _canDeviceTakePhoto = false; _isSelectingToothArea = true; String? area = await showDialog( context: context, barrierDismissible: false, builder: (ctx) { return _getToothSelect(ctx, (String area) { showToast(text: getS().hasSelectXx(toothAreaTranslate(area))); Navigator.pop(ctx, area); }); }); _canDeviceTakePhoto = true; _isSelectingToothArea = false; if (area == null) { return; } setState(() { _currentToothArea = area; }); } Widget _getToothSelect( BuildContext context, void Function(String area) onSelect) { return PopScope( canPop: false, child: FittedBox( child: Container( margin: EdgeInsets.all(80.r), padding: EdgeInsets.symmetric(horizontal: 100.w, vertical: 50.h), decoration: BoxDecoration( color: Colors.white30, borderRadius: BorderRadius.circular(100.r)), child: Row( mainAxisSize: MainAxisSize.min, children: [ Column( mainAxisSize: MainAxisSize.min, children: [ GestureDetector( child: Image.asset(Assets.imagesToothRt), onTap: () => onSelect(toothAreaRightTop), ), GestureDetector( child: Image.asset(Assets.imagesToothRb), onTap: () => onSelect(toothAreaRightBottom), ), ], ), Column( mainAxisSize: MainAxisSize.min, children: [ GestureDetector( child: Image.asset(Assets.imagesToothLt), onTap: () => onSelect(toothAreaLeftTop), ), GestureDetector( child: Image.asset(Assets.imagesToothLb), onTap: () => onSelect(toothAreaLeftBottom), ), ], ), ], ), ), )); } }