video_view_page.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. import 'dart:io';
  2. import 'package:auto_route/auto_route.dart';
  3. import 'package:bot_toast/bot_toast.dart';
  4. import 'package:eitc_erm_dental_flutter/app_router.gr.dart';
  5. import 'package:eitc_erm_dental_flutter/db_util.dart';
  6. import 'package:eitc_erm_dental_flutter/entity/db/local_patient_info.dart';
  7. import 'package:eitc_erm_dental_flutter/exts.dart';
  8. import 'package:eitc_erm_dental_flutter/global.dart';
  9. import 'package:eitc_erm_dental_flutter/pages/view/widget/video_patient_info_bar.dart';
  10. import 'package:eitc_erm_dental_flutter/sp_util.dart';
  11. import 'package:eitc_erm_dental_flutter/vm/global_view_model.dart';
  12. import 'package:flutter/material.dart';
  13. import 'package:flutter/services.dart';
  14. import 'package:flutter_riverpod/flutter_riverpod.dart';
  15. import 'package:flutter_screenutil/flutter_screenutil.dart';
  16. import 'package:just_audio/just_audio.dart';
  17. import 'package:wakelock_plus/wakelock_plus.dart';
  18. import '../../funcs.dart';
  19. import '../../generated/assets.dart';
  20. import 'widget/video_operation_view.dart';
  21. ///视频查看页面
  22. @RoutePage(name: "videoViewRoute")
  23. class VideoViewPage extends ConsumerStatefulWidget {
  24. const VideoViewPage({super.key});
  25. @override
  26. ConsumerState createState() => _VideoViewPageState();
  27. }
  28. class _VideoViewPageState extends ConsumerState<VideoViewPage>
  29. with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
  30. ///纹理ID
  31. int _textureId = -1;
  32. ///视频控制界面控制器
  33. final VideoOperationViewController _operationViewController =
  34. VideoOperationViewController();
  35. ///上一次拍照的路径
  36. String _lastTakePhotoPath = "";
  37. ///音频播放器
  38. final AudioPlayer _takePhotoAudioPlayer = AudioPlayer();
  39. ///是否已弹出
  40. bool _hasPoped = false;
  41. ///已选择的咨询人信息
  42. LocalPatientInfo? _patientInfo;
  43. ///wifi名称
  44. String _wifiName = "";
  45. ///设备型号
  46. String _deviceModel = "";
  47. ///当前牙齿区域
  48. String _currentToothArea = "";
  49. ///是否可以设备拍照
  50. bool _canDeviceTakePhoto = true;
  51. ///是否正在选择牙齿区域
  52. bool _isSelectingToothArea = false;
  53. @override
  54. void initState() {
  55. super.initState();
  56. WidgetsBinding.instance.addObserver(this);
  57. //保持屏幕常亮
  58. WakelockPlus.enable();
  59. //开启屏幕旋转
  60. screenEnableRotate();
  61. //设置MethodChannel回调
  62. videoChannel.setMethodCallHandler(_methodCallHandler);
  63. //音频播放器加载资源
  64. _takePhotoAudioPlayer.setAsset(Assets.audiosTakePhoto);
  65. //开始视频
  66. _startVideo();
  67. //监听连接状态变化
  68. ref.listenManual(deviceConnectStatusProvider(videoChannel), (_, bo) {
  69. if (!bo) {
  70. logd("连接丢失");
  71. _popSelf();
  72. }
  73. });
  74. //初始化数据
  75. _initData();
  76. WidgetsBinding.instance.addPostFrameCallback((_) {
  77. //选择牙齿区域
  78. _selectToothArea();
  79. });
  80. }
  81. ///初始化数据
  82. void _initData() async {
  83. //初始化咨询人信息,设备型号
  84. await Future.wait([_initPatientInfo(), _initDeviceModel()]);
  85. setState(() {});
  86. }
  87. ///初始化咨询人信息
  88. Future _initPatientInfo() async {
  89. if (selectedPatientId < 0) {
  90. return;
  91. }
  92. LocalPatientInfo? info =
  93. await DbUtil.instance.getLocalPatientById(selectedPatientId);
  94. if (info != null) {
  95. _patientInfo = info;
  96. }
  97. }
  98. ///初始化设备型号
  99. Future _initDeviceModel() async {
  100. String? name = await getWifiName();
  101. if (name.isNullOrEmpty) {
  102. return;
  103. }
  104. _wifiName = name!;
  105. _deviceModel = await getDeviceModel() ?? "";
  106. logd("初始化设备型号,wifiName=$_wifiName,deviceModel=$_deviceModel");
  107. }
  108. @override
  109. void dispose() {
  110. WidgetsBinding.instance.removeObserver(this);
  111. //关闭屏幕常亮
  112. WakelockPlus.disable();
  113. if (!_hasPoped) {
  114. _stopRecord();
  115. }
  116. _stopVideo();
  117. videoChannel.setMethodCallHandler(null);
  118. //关闭屏幕旋转
  119. screenDisableRotate();
  120. _operationViewController.dispose();
  121. super.dispose();
  122. }
  123. @override
  124. void didChangeAppLifecycleState(AppLifecycleState state) {
  125. if (!(state == AppLifecycleState.inactive ||
  126. state == AppLifecycleState.resumed)) {
  127. _popSelf();
  128. }
  129. }
  130. @override
  131. bool get wantKeepAlive => true;
  132. @override
  133. Widget build(BuildContext context) {
  134. super.build(context);
  135. return Scaffold(
  136. body: SafeArea(
  137. child: Container(
  138. color: Colors.black,
  139. child: Stack(
  140. children: [
  141. Center(
  142. child: _getVideo(),
  143. ),
  144. VideoOperationView(
  145. controller: _operationViewController,
  146. onTakePhoto: _takePhoto,
  147. onStartRecord: _startRecord,
  148. onStopRecord: () => _stopRecord(toPreview: true),
  149. onStopVideo: _stopVideo,
  150. area: _currentToothArea,
  151. ),
  152. Align(
  153. alignment: Alignment.topLeft,
  154. child: _getVideoViewToolBar(),
  155. )
  156. ],
  157. ),
  158. )),
  159. );
  160. }
  161. void _startVideo() async {
  162. if (!mounted) {
  163. return;
  164. }
  165. try {
  166. int result = await videoChannel.invokeMethod("startVideo");
  167. setState(() {
  168. _textureId = result;
  169. });
  170. } catch (e) {
  171. showToast(text: getS().showVideoError);
  172. loge("开始视频失败", error: e);
  173. }
  174. }
  175. void _stopVideo() async {
  176. logd("停止视频");
  177. if (_textureId < 0) {
  178. return;
  179. }
  180. _textureId = -1;
  181. try {
  182. await videoChannel.invokeMethod("stopVideo");
  183. } catch (e) {
  184. loge("停止视频失败", error: e);
  185. }
  186. }
  187. Future<dynamic> _methodCallHandler(MethodCall call) {
  188. logd(call);
  189. if (call.method == "onDeviceTakePhoto") {
  190. _onDeviceTakePhoto(call.arguments);
  191. }
  192. return Future.value("");
  193. }
  194. ///获取视频Widget
  195. Widget _getVideo() {
  196. return OrientationBuilder(builder: (ctx, orientation) {
  197. if (Platform.isIOS) {
  198. Size size = MediaQuery.of(context).size;
  199. double width = size.width;
  200. double height = size.height;
  201. videoChannel.invokeMethod(
  202. orientation == Orientation.landscape
  203. ? "getLandscapeView"
  204. : "getPortraitView",
  205. [width, height]);
  206. logd("ios,width=$width,height=$height");
  207. if (orientation == Orientation.landscape) {
  208. return AspectRatio(
  209. aspectRatio: 16.0 / 9.0,
  210. child: Stack(
  211. children: [
  212. _getVideoView(true),
  213. Align(
  214. alignment: Alignment.bottomCenter,
  215. child: _getPatientInfoBar(orientation),
  216. )
  217. ],
  218. ),
  219. );
  220. } else {
  221. return Column(
  222. mainAxisSize: MainAxisSize.min,
  223. children: [
  224. AspectRatio(
  225. aspectRatio: 16.0 / 9.0,
  226. child: _getVideoView(true),
  227. ),
  228. _getPatientInfoBar(orientation),
  229. ],
  230. );
  231. }
  232. } else {
  233. if (orientation == Orientation.landscape) {
  234. return AspectRatio(
  235. aspectRatio: 16.0 / 9.0,
  236. child: Stack(
  237. children: [
  238. _getVideoView(false),
  239. Align(
  240. alignment: Alignment.bottomCenter,
  241. child: _getPatientInfoBar(orientation),
  242. )
  243. ],
  244. ),
  245. );
  246. } else {
  247. return Column(
  248. mainAxisSize: MainAxisSize.min,
  249. children: [
  250. AspectRatio(
  251. aspectRatio: 16.0 / 9.0,
  252. child: _getVideoView(false),
  253. ),
  254. _getPatientInfoBar(orientation)
  255. ],
  256. );
  257. }
  258. }
  259. });
  260. }
  261. Widget _getVideoView(bool isIos) {
  262. if (isIos) {
  263. return UiKitView(
  264. viewType: "VideoView",
  265. onPlatformViewCreated: (id) => logd("IOS PlatformView创建,id=$id"),
  266. );
  267. } else {
  268. return _textureId >= 0
  269. ? Texture(textureId: _textureId)
  270. : const SizedBox();
  271. }
  272. }
  273. ///获取患者信息条
  274. Widget _getPatientInfoBar(Orientation orientation) {
  275. return _operationViewController.isRecording
  276. ? SizedBox()
  277. : VideoPatientInfoBar(
  278. info: _patientInfo,
  279. orientation: orientation,
  280. area: _currentToothArea,
  281. deviceModel: _deviceModel);
  282. }
  283. ///获取工具栏
  284. Widget _getVideoViewToolBar() {
  285. return Row(
  286. children: [
  287. IconButton(
  288. onPressed: () => _popSelf(true),
  289. icon: Icon(
  290. Platform.isAndroid ? Icons.arrow_back : Icons.arrow_back_ios_new,
  291. color: Colors.white,
  292. )),
  293. const Spacer(),
  294. IconButton(
  295. onPressed: _gotoSettings,
  296. icon: const Icon(
  297. Icons.settings_outlined,
  298. color: Colors.white,
  299. )),
  300. ],
  301. );
  302. }
  303. ///拍照
  304. void _takePhoto() async {
  305. if (_operationViewController.isRecording) {
  306. return;
  307. }
  308. try {
  309. String path = await videoChannel.invokeMethod("takePhoto");
  310. _showTakePhotoSuccess(path);
  311. } catch (e) {
  312. loge("拍照失败", error: e);
  313. showToast(text: getS().takePhotoFailed);
  314. }
  315. }
  316. ///当设备拍照
  317. ///IOS平台设备和手机拍照都会调用
  318. void _onDeviceTakePhoto(dynamic arguments) async {
  319. if (!_canDeviceTakePhoto || !isCurrentPage) {
  320. logd("设备拍照,但是不可拍照,返回");
  321. return;
  322. }
  323. _takePhoto();
  324. }
  325. void _showTakePhotoSuccess(String newPath) async {
  326. if (newPath == _lastTakePhotoPath) {
  327. return;
  328. }
  329. logd("拍照成功,path=$newPath");
  330. _lastTakePhotoPath = newPath;
  331. showToast(text: getS().takePhotoSuccess);
  332. _playTakePhotoAudio();
  333. String area = _currentToothArea;
  334. if (mounted) {
  335. setState(() {
  336. _currentToothArea = "";
  337. _stopVideo();
  338. });
  339. await context.pushRoute(PhotoPreviewRoute(
  340. info: _patientInfo!,
  341. path: newPath,
  342. area: area,
  343. time: DateTime.now(),
  344. mobile: "",
  345. deviceModel: _deviceModel,
  346. wifi: _wifiName));
  347. _selectToothArea();
  348. _startVideo();
  349. }
  350. }
  351. ///开始录像
  352. void _startRecord() async {
  353. if (_operationViewController.isRecording) {
  354. return;
  355. }
  356. _canDeviceTakePhoto = false;
  357. try {
  358. await videoChannel.invokeMethod(
  359. "startRecord",
  360. makeFilePrefix(
  361. name: _patientInfo?.name ?? "",
  362. idCard: _patientInfo?.idCardDecrypt ?? "",
  363. mobile: "",
  364. area: _currentToothArea,
  365. wifi: _wifiName,
  366. time: DateTime.now().millisecondsSinceEpoch,
  367. userId: await SpUtil.getUserId()));
  368. _operationViewController.updateRecordingState(true);
  369. showToast(text: getS().hasStartRecord);
  370. setState(() {});
  371. } catch (e) {
  372. _canDeviceTakePhoto = true;
  373. _operationViewController.updateRecordingState(false);
  374. loge("开始录像失败", error: e);
  375. showToast(text: getS().startRecordFailed);
  376. }
  377. }
  378. ///停止录像
  379. void _stopRecord({bool toPreview = false}) async {
  380. if (!_operationViewController.isRecording) {
  381. return;
  382. }
  383. logd("停止录像");
  384. _canDeviceTakePhoto = true;
  385. try {
  386. String path = await videoChannel.invokeMethod("stopRecord");
  387. if (toPreview) {
  388. //显示loading延迟停止视频,因为停止录像是异步的,如果在没执行完就停止视频
  389. //会导致录像文件丢失尾帧信息,播放时没有总时长
  390. var cancel = BotToast.showLoading(
  391. crossPage: false,
  392. clickClose: false,
  393. backButtonBehavior: BackButtonBehavior.ignore);
  394. await Future.delayed(Duration(seconds: 1, milliseconds: 500));
  395. cancel();
  396. showToast(text: getS().hasStopRecord);
  397. setState(() {
  398. _operationViewController.updateRecordingState(false);
  399. _stopVideo();
  400. });
  401. if (mounted) {
  402. await context.pushRoute(VideoPreviewRoute(
  403. info: _patientInfo!,
  404. path: path,
  405. area: _currentToothArea,
  406. time: DateTime.now(),
  407. mobile: "",
  408. deviceModel: _deviceModel,
  409. wifi: _wifiName));
  410. _selectToothArea();
  411. _startVideo();
  412. }
  413. } else {
  414. _operationViewController.updateRecordingState(false);
  415. showToast(text: getS().hasStopRecord);
  416. }
  417. } catch (e) {
  418. _operationViewController.updateRecordingState(false);
  419. loge("停止录像失败", error: e);
  420. showToast(text: getS().stopRecordFailed);
  421. }
  422. }
  423. ///播放拍照音效
  424. void _playTakePhotoAudio() async {
  425. await _takePhotoAudioPlayer.seek(Duration.zero);
  426. await _takePhotoAudioPlayer.play();
  427. }
  428. ///前往设置
  429. void _gotoSettings() async {
  430. await context.pushRoute(const DelayShotSettingsRoute());
  431. if (_hasPoped) {
  432. return;
  433. }
  434. screenEnableRotate();
  435. }
  436. ///弹出自身页面
  437. void _popSelf([bool force = false]) {
  438. if (force) {
  439. Navigator.pop(context);
  440. return;
  441. }
  442. if (_hasPoped) {
  443. return;
  444. }
  445. _hasPoped = true;
  446. _stopRecord();
  447. //如果是锁屏导致的,dispose会在解锁后才会调用,导致在锁屏期间视频还是播放的,所以这里提前停止
  448. _stopVideo();
  449. //如果当前页面是最上层的就是pop,否则remove,否则会导致把后来打开的页面pop但是当前页面还存在的问题
  450. if (isCurrentPage) {
  451. logd("视频查看页面pop自身");
  452. //如果正在选择牙齿区域,先把弹窗pop掉
  453. if (_isSelectingToothArea) {
  454. Navigator.pop(context);
  455. }
  456. Navigator.pop(context);
  457. } else {
  458. logd("视频查看页面remove自身");
  459. AutoRouter.of(context).removeRoute(context.routeData);
  460. }
  461. }
  462. ///是否是当前页面
  463. bool get isCurrentPage =>
  464. AutoRouter.of(context).current.name == VideoViewRoute.name;
  465. ///选择延迟区域
  466. void _selectToothArea() async {
  467. if (!mounted) {
  468. return;
  469. }
  470. _canDeviceTakePhoto = false;
  471. _isSelectingToothArea = true;
  472. String? area = await showDialog(
  473. context: context,
  474. barrierDismissible: false,
  475. builder: (ctx) {
  476. return _getToothSelect(ctx, (String area) {
  477. showToast(text: getS().hasSelectXx(toothAreaTranslate(area)));
  478. Navigator.pop(ctx, area);
  479. });
  480. });
  481. _canDeviceTakePhoto = true;
  482. _isSelectingToothArea = false;
  483. if (area == null) {
  484. return;
  485. }
  486. setState(() {
  487. _currentToothArea = area;
  488. });
  489. }
  490. Widget _getToothSelect(
  491. BuildContext context, void Function(String area) onSelect) {
  492. return PopScope(
  493. canPop: false,
  494. child: FittedBox(
  495. child: Container(
  496. margin: EdgeInsets.all(80.r),
  497. padding: EdgeInsets.symmetric(horizontal: 100.w, vertical: 50.h),
  498. decoration: BoxDecoration(
  499. color: Colors.white30,
  500. borderRadius: BorderRadius.circular(100.r)),
  501. child: Row(
  502. mainAxisSize: MainAxisSize.min,
  503. children: [
  504. Column(
  505. mainAxisSize: MainAxisSize.min,
  506. children: [
  507. GestureDetector(
  508. child: Image.asset(Assets.imagesToothRt),
  509. onTap: () => onSelect(toothAreaRightTop),
  510. ),
  511. GestureDetector(
  512. child: Image.asset(Assets.imagesToothRb),
  513. onTap: () => onSelect(toothAreaRightBottom),
  514. ),
  515. ],
  516. ),
  517. Column(
  518. mainAxisSize: MainAxisSize.min,
  519. children: [
  520. GestureDetector(
  521. child: Image.asset(Assets.imagesToothLt),
  522. onTap: () => onSelect(toothAreaLeftTop),
  523. ),
  524. GestureDetector(
  525. child: Image.asset(Assets.imagesToothLb),
  526. onTap: () => onSelect(toothAreaLeftBottom),
  527. ),
  528. ],
  529. ),
  530. ],
  531. ),
  532. ),
  533. ));
  534. }
  535. }